메뉴
HN
Hacker News 17일 전

캠퍼스 내 모든 프로젝터와 카메라 제어권 획득하기

IMP
8/10
핵심 요약

미국 콜로라도 광물대학교(Colorado School of Mines) 학생이 학교 내 DNS 서버가 각 기기에 서브도메인을 자동 할당한다는 점을 악용해, 무차별 대입(Brute force) 공격으로 캠퍼스 내 프로젝터와 카메라를 비롯한 기기들을 추적하고 제어하는 방법을 다룬 해킹 사례입니다. 처음에는 Python으로 시작했으나 처리 속도의 한계에 부딪혔고, 결국 Rust와 36진수 변환 알고리즘을 도입하여 프로그램의 성능을 극대화하여 작업을 마무리했습니다. 이 글은 네트워크 인프라의 취약한 설정이 어떻게 전체 캠퍼스 기기의 보안 위협으로 이어질 수 있는지 보여주는 중요한 사례입니다.

번역된 본문

돌아가기 ⬏ 캠퍼스 내 모든 프로젝터와 카메라 제어권 획득하기 게시일: 2026년 5월 13일

콜로라도 광물대학교(Colorado School of Mines)에서 첫 학년을 시작할 무렵, 꽤 흥미로운 사실을 하나 발견했습니다. 바로 로컬 DNS 서버가 네트워크에 연결되는 각 기기에 서브도메인을 할당한다는 것입니다. 즉, “meow”라는 이름의 기기는 캠퍼스 내 모든 Wi-Fi에서 “meow.mines.edu”로 보이게 됩니다. 물론 네트워크 자체에서 외부 트래픽은 차단하지만 말입니다. 이 사실은 제 머릿속에 남아 이를 통해 무엇을 할 수 있을지, 특히 이 방법을 통해 네트워크 내 기기를 역추적할 수 있을지 생각하게 만들었습니다.

여기서 가장 큰 문제는 특정 서브도메인을 특정 IP로 연결해 주는 근본적인 DNS 레코드에 접근하는 것입니다(실제로는 이보다 조금 더 복잡하지만, 요점은 그렇습니다). 이 문제는 몇 가지 방법으로 해결할 수 있습니다.

영역 전송 (Zone Transfer) 이것이 가장 간단한 옵션이겠지만, IT 부서가 저에게 간단히 영역 전송을 허용해 줄 것이라고는 생각하지 않습니다(그리고 그들은 누구에게나 영역 전송을 허용해서는 안 됩니다). 또한 이 방법은 네트워크에 새 기기가 추가되거나 제거될 때마다 제 목록이 업데이트되지 않는 단점이 있습니다. 게다가, 그게 뭐가 재미있겠습니까?

인증서 (Certificates) 많은 서브도메인이 데이터가 올바른 곳에서 오는지 확인하기 위해 인증서(TLS, HTTPS 인증서)를 등록합니다. 이러한 인증서는 Let's Encrypt의 CT 로그 같은 추가 전용 원장(Ledger)이나 로그에 기록됩니다. 이러한 시스템은 웹사이트에는 유용하고 필수적이지만, 대학이 Wi-Fi에 연결된 모든 기기에 대해 TLS 인증서를 등록할 이유가 없다고 생각합니다. 특히 그 기기들은 인터넷에서 접근조차 불가능하기 때문입니다.

무차별 대입 (Brute force) 제가 생각하는 마지막 접근법이자 이 블로그 글의 주제는 무차별 대입(Brute force)입니다. 가능한 모든 도메인을 검색하거나, 일반적인 호스트 이름 사전(Dictionary)을 사용하여 탐색 횟수를 줄이는 등 몇 가지 방법이 있습니다. 개인적으로 이 프로젝트에서는 사전 접근 방식이 마음에 들지 않았습니다. 실제 현실에서는 더 실용적일지 몰라도, 저는 모든 조합을 무차별 대입으로 찾아낼 만큼 충분히 빠른 프로그램을 작성하는 데 특별히 관심이 생렸기 때문입니다. 가능한 도메인의 수가 '37의 n승'(n은 서브도메인의 문자 길이)이기 때문에 이 작업은 기하급수적으로 어려워집니다.

무차별 대입을 통한 순열 생성을 선택한 후(아마도 최악의 선택이었을 수 있습니다), 본격적으로 작업에 들어갔습니다. 첫 번째 프로그램은 문제를 이해하기 위해 Python으로 작성했습니다. itertools의 순열 함수를 사용하여 서브도메인을 만들기 위한 모든 문자와 숫자의 조합을 찾았습니다(스포일러를 밝히자면, 이게 바로 병목이었습니다). 각 DNS 응답을 기다리느라 실행이 차단되는 것을 막기 위해 비동기(Async) 방식이 필요하다는 것을 알고 있었습니다. 프로그램 자체로는 괜찮았고 쓸만한 최소한의 TUI(텍스트 사용자 인터페이스)를 갖추고 있었지만, 비동기임에도 불구하고 너무 느렸습니다. 그럼에도 불구하고 합리적인 시간 내에 3개의 문자로 구성된 서브도메인을 나열하기에는 충분했습니다.

바로 이즈음에 저는 언어를 Rust로 옮기기 시작했습니다. 저는 Rust로 된 멋진 프로젝트들을 많이 알고 있었지만, 제가 직접 뭔가를 만들어야 할 필요성은 느끼지 못했습니다. 지금까지 제가 하는 일에는 항상 Python이 충분히 빨랐으니까요. 또한 모달 편집기인 Helix를 가지고 놀기 시작하면서 그것이 얼마나 마음에 드는지 깨달았습니다. 저는 Python을 쓸 때 구문 강조 이상의 기능을 가진 편집기를 거의 사용하지 않지만, Rust에서는 언어 서버(Language Server)가 필수적이게 됩니다.

첫 시도에서는 Rust의 itertools 크레이트(crate)에 있는 다중 카티션 곱(multi cartesian product) 함수를 사용했지만, 곧 정수를 증가시킨 다음 이를 36진수로 변환하는 것이 훨씬 더 빠를 것이라는 것을 깨달았습니다. 순열 스레드의 속도는 상당히 중요해졌는데, 이는 이 부분이 대량의 CPU 작업을 수행하는 유일한 요소이며 쉽게 병렬화할 수 없기 때문입니다. Python 버전에서는 동일한 순열 생성 방식을 사용해 접두사를 만들고 각각을 스레드에 할당했지만, Rust에서는 이런 방식을 다루고 싶지 않았습니다. 대신, Bash 스크립트를 사용해 프로그램에 여러 프로세스를 생성하도록 하고, 각 프로세스에 총 프로세스 수와 고유한 오프셋(Offset)을 알려주었습니다. 각 프로세스는 IP를 나타내는 정수를 1 대신 총 프로세스 수만큼 증가시키며 시작하게 했습니다.

원문 보기
원문 보기 (영어)
Back ⬏ Gaining control of every projector and camera on campus Published: May 13, 2026 While starting my first year at the Colorado School of Mines, I came across a rather interesting fact: the local DNS servers will assign a subdomain to each device that connects to the network. This means that a device called “meow” will be visible as “meow.mines.edu” on any campus wifi, though the network still blocks any traffic. This got me thinking in the back of my mind about what I could do with this, and especially if I could use this to trace back devices on the network. The main problem is accessing the underlying DNS records that point a specific subdomain to a specific IP (it’s a bit more complicated than this, but you get the gist), which can be addressed in a few ways: Zone Transfer While this would be the simplest option, I doubt IT would simply zone transfer to me (and they really should not zone transfer to just anyone). My list also wouldn’t update as new devices are added or removed from the network. Plus, where would be the fun in that? Certificates Many subdomains register certifications to ensure your data is coming from the right place (TLS, HTTPS certificates). These certificates are added to append only ledgers or logs, like letsencrypts CT logs. While these are useful and necessary for websites, I don’t see why my college would register TLS certificates for every device connected to its wifi, especially since they aren’t even accessible from the internet. Brute force The last approach I see, and the one this blog is all about, is brute force. There’s a few ways to do this, including searching every possible domain, or using a dictionary of common hostnames to reduce the number of lookups. I personally don’t like the dictionary approach for this project. Even though it’s more practical in the real world, I became particularly interested in writing a program that was fast enough to brute force every combination. This gets exponentially harder, as the number of domains possible is 37^n, where n is the character length of the subdomains. After settling on permuting through brute force (possibly the worst option), I got to work. The first program was just something I wrote in python, getting me to wrap my head around the problem. I used the itertools permutation function to find every combination of letters and numbers to make the subdomain (spoiler alert, this was a bottleneck). I knew it needed to be async because otherwise I would be blocking in order to wait for each DNS response. It was fine as a program, and had a decent minimal TUI, but was way too slow despite being async. Still enough to enumerate 3 characters in a decent amount of time. This was about when I started moving over to rust. I know a lot of cool projects in rust, yet I never saw the need to build something in it. Python had always been fast enough for what I was doing, until now. I also started playing around with Helix , a modal editor, and realized how much I like it. While I rarely use any editor that goes beyond syntax highlighting for python, language servers also become a necessity in rust. My first attempt used a multi cartesian product function from the rust itertools crate, but I realized shortly later that incrementing an integer and then converting it to base 36 would be much faster. The speed of the permutation thread became rather important, as it’s the only piece that does a large amount of CPU work and can’t be easily parallelized. In the python version, I generated prefixes using the same permutation and then assigned each to a thread, but I really didn’t want to deal with this in rust. Instead, I created multiple processes for my program using a bash script, and informed each of the total number of processes and its specific offset. Each process would increment the integer representing the IP by the total number of processes (instead of 1), starting at its offset value, therefore covering every value between all processes. In rust, I got direct access to the udp port handling the dns server connection, which meant reading and writing could happen within different async functions. I assigned the reader to continue in a loop until it had read from the socket as many times as I sent requests, and only print out the ones that did not respond with NXDOMAIN (Domain doesn’t exist). Handling this here is rather important, as early on I was using grep to filter out domains and it started eating almost as much CPU as the process itself. The writer is rather simple, it converts the integer to the base36 value, writes the DNS query to the socket, and then exits. At one point, I encountered a rather strong memory leak that would get my process up to hundreds of GB of ram. This was happening due to me appending each writer function call to a list, so that at the end of the process I could wait for all writer functions to exit. To fix this, I first tried dropping these handles and instead waiting for the reader to finish, but this still did not stop the leak. I concluded that the internal async stack within tokio (one of the rust async libraries) was actually what was leaking, as I was spawning queries faster than they could be moved to be executed. To fix this part, I blocked queries once I got to a certain amount and continued after all were done. A perfect solution would be to keep the async stack at a certain size constantly. In the real world, this would make a rather small performance improvement while needing me to rewrite a lot of my program, so I kept it as it is. Through optimizing, I eventually got the program up to 200 Mibps per 2 threads (The thread count is rather important as the server I was working on is limited to 500 threads, and only has 96 cores to work with anyways). This calculates to about 50.33 Gbps with 480 threads, but the last version I tested at full capacity only hit 4.04 Gbps, and I would have been limited to around 96 threads (or about 10.06 Gbps) capacity anyways. At some point, I hit a threshold where the DNS server could no longer keep up and broke. As I later found out, this caused a ~15 minute campus wide outage for managed computers as no computer could make the DNS lookup in order to mount its network drive. IT politely told me to stop spamming the DNS server after this, so I did. How’d IT know it was me? I yapped about it for two weeks! Now, that gives me a decent chunk of the subdomains, but only until processing 37 to the n queries becomes unrealistic. Luckily, this is where I learned of another secret fourth option: PTR Records This is another form of DNS record that maps an IP back to a domain. For example, I could map 10.0.0.0 back to meow.mines.edu . This means that I only have to scan as many IPs as are assigned within my network. For this part, I wrote a much simpler rust script that sent a DNS query for every domain I knew my college owned, or was in the network. This worked pretty well, and I got to see the silly devices on the network. Unfortunately most of these are uninteresting, like computer-precision-tower-5810 :( Now that I know every computer.. what could I do with them? Now most of the time the network does not allow you to open a connection to anything that’s not an IT server. Under specific circumstances that I won’t get into, it will allow you to. This made me want to know more, so I started building a port scanner. This started with something simple that used tokio’s TcpStream implementation for networking, simply checking if I can connect and then dropping the connection. It enumerated through every IP in a /16 subnet and scanned a set of ports, blocking so that only ~4000 ports were scanned per machine at any time. This worked fine, but it took a lot of CPU to get anywhere, and I knew I could go faster. Around this time I started looking into AF_XDP, a part of the linux kernel that allows creat